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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/buildcheck/natives.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
uploads a smoke set llama-smoke-<target> per smoke target, and every smoke
target is launched by a job that downloads its set
* LlamaLoader LIBRARY_BACKENDS / MODULE_BACKENDS know every backend of their kind (else a
jar ships and never loads)
jar ships and never loads), and OPT_IN_BACKENDS names exactly the backends
the optin column declares (else an opt-in jar takes over silently, or an
inert one has no row saying why)
* CMakeLists.txt names exactly the backend directories of the list
* nativedeps.ALLOWED holds an allowlist for every library directory (cpu, metal), and for no
directory the list lacks
Expand Down Expand Up @@ -137,6 +139,11 @@ def check_rows(natives):
if not r["classifier"].startswith(backend(r) + "-") or r["platform"] not in ("yes", "no"):
failures.append(f"natives.csv: row {r['classifier']} -- classifier must start with its "
f"directory's backend '{backend(r)}', platform must be yes or no")
if r.get("optin") not in ("yes", "no"):
failures.append(f"natives.csv: row {r['classifier']} -- optin must be yes or no")
elif r["optin"] == "yes" and r.get("kind") != "module":
failures.append(f"natives.csv: row {r['classifier']} -- only a module can be opt-in: a library jar "
f"is the one jar that must load")
if r.get("kind") not in KINDS:
failures.append(f"natives.csv: row {r['classifier']} -- kind must be one of {KINDS}")
elif r["kind"] == "library" and not re.fullmatch(r"(lib)?jllama\.(so|dll|dylib)", r["library"]):
Expand All @@ -150,6 +157,11 @@ def check_rows(natives):
failures.append(f"natives.csv: row {r['classifier']} -- a module cannot be a platform jar: it "
f"holds no library")
trees.setdefault(tree(r), []).append(r)
for b in sorted({backend(r) for r in natives}):
answers = {r.get("optin") for r in natives if backend(r) == b}
if len(answers) > 1:
failures.append(f"natives.csv: backend '{b}' is opt-in in one row and not in another "
f"({sorted(answers)}) -- the loader decides per backend, so the rows cannot differ")
for t, group in sorted(trees.items()):
if not any(r.get("kind") == "library" for r in group):
failures.append(f"natives.csv: {t} has module jars but no library jar -- nothing would load them")
Expand Down Expand Up @@ -238,6 +250,16 @@ def constant(name):
block = re.search(name + r"\s*=(.*?);", loader_text, re.S)
return set(re.findall(r'"([^"]+)"', block.group(1))) if block else set()
failures = []
# OPT_IN_BACKENDS is what makes a module jar inert until the property names it, so a backend the
# list declares opt-in while the loader does not would ship a jar that takes over silently --
# which is the measured OpenVINO regression this column exists for -- and the other way round a
# jar would never load with no row saying why.
listed_optin = {backend(r) for r in natives if r.get("optin") == "yes"}
known_optin = constant("OPT_IN_BACKENDS")
failures += [f"LlamaLoader.OPT_IN_BACKENDS does not know '{b}', which natives.csv declares opt-in"
for b in sorted(listed_optin - known_optin)]
failures += [f"LlamaLoader.OPT_IN_BACKENDS names '{b}', which no row of natives.csv declares opt-in"
for b in sorted(known_optin - listed_optin)]
for kind, const in (("library", "LIBRARY_BACKENDS"), ("module", "MODULE_BACKENDS")):
listed = {backend(r) for r in natives if r["kind"] == kind}
known = constant(const)
Expand Down
87 changes: 60 additions & 27 deletions .github/buildcheck/tests/test_natives.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
from buildcheck.tests.helpers import REPO, upload, workflow_text

CSV = """# a comment
classifier,directory,library,platform,kind
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library
classifier,directory,library,platform,kind,optin
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module,no
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library,no
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library,no
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module,no
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library,no
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module,yes
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library,no
"""

ROWS = natives.rows(CSV)
Expand Down Expand Up @@ -67,7 +67,8 @@ class ListTest(unittest.TestCase):

def test_rows_skip_comments(self):
self.assertEqual(ROWS[0], {"classifier": "cpu-linux-x86-64", "directory": "Linux/x86_64/cpu",
"library": "libjllama.so", "platform": "yes", "kind": "library"})
"library": "libjllama.so", "platform": "yes", "kind": "library",
"optin": "no"})
self.assertEqual(len(ROWS), 8)
self.assertEqual([r["classifier"] for r in natives.modules(ROWS)],
["cuda13-linux-x86-64", "vulkan-windows-aarch64", "opencl-android-aarch64"])
Expand All @@ -86,10 +87,10 @@ def test_smoke_targets_are_the_desktop_library_platforms(self):

def test_check_rows(self):
self.assertEqual(natives.check_rows(ROWS), [])
bad = natives.rows("classifier,directory,library,platform,kind\n"
"cuda-linux-x86-64,Linux/x86_64/cuda13,x,no,module\n"
"cpu-a,A/b/cpu,libjllama.so,maybe,library\n"
"cpu-a,A/b/cpu,libjllama.so,no,thing\n")
bad = natives.rows("classifier,directory,library,platform,kind,optin\n"
"cuda-linux-x86-64,Linux/x86_64/cuda13,x,no,module,no\n"
"cpu-a,A/b/cpu,libjllama.so,maybe,library,no\n"
"cpu-a,A/b/cpu,libjllama.so,no,thing,no\n")
failures = natives.check_rows(bad)
self.assertEqual(len(failures), 7, failures)
self.assertIn("twice", failures[0])
Expand All @@ -102,20 +103,20 @@ def test_check_rows(self):
self.assertTrue(any(expected in f for f in failures), expected)

def test_check_rows_refuses_a_module_without_a_library_and_colliding_file_names(self):
orphan = natives.rows("classifier,directory,library,platform,kind\n"
"vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module\n")
orphan = natives.rows("classifier,directory,library,platform,kind,optin\n"
"vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module,no\n")
self.assertEqual(natives.check_rows(orphan),
["natives.csv: Linux/aarch64 has module jars but no library jar -- nothing would load them"])
twice = natives.rows("classifier,directory,library,platform,kind\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library\n"
"sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module\n"
"sycl-fp32-linux-x86-64,Linux/x86_64/sycl-fp32,libggml-sycl.so,no,module\n")
twice = natives.rows("classifier,directory,library,platform,kind,optin\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no\n"
"sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module,no\n"
"sycl-fp32-linux-x86-64,Linux/x86_64/sycl-fp32,libggml-sycl.so,no,module,no\n")
failures = natives.check_rows(twice)
self.assertEqual(len(failures), 1, failures)
self.assertIn("same name", failures[0])
platform_module = natives.rows("classifier,directory,library,platform,kind\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library\n"
"cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,yes,module\n")
platform_module = natives.rows("classifier,directory,library,platform,kind,optin\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no\n"
"cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,yes,module,no\n")
self.assertIn("cannot be a platform jar", natives.check_rows(platform_module)[0])


Expand Down Expand Up @@ -170,22 +171,54 @@ def test_a_matrix_launches_its_rows(self):
["publish.yml smoke runs (a job downloading llama-smoke-<target>): missing windows-aarch64"])

def test_a_module_jar_without_a_smoke_target_is_refused(self):
rows = natives.rows(CSV + "vulkan-linux-s390x,Linux/s390x/vulkan,libggml-vulkan.so,no,module\n")
rows = natives.rows(CSV + "vulkan-linux-s390x,Linux/s390x/vulkan,libggml-vulkan.so,no,module,no\n")
failures = natives.check_workflow(rows, jobs(rows=rows))
self.assertEqual(failures, ["natives.csv: the module jar vulkan-linux-s390x has no smoke target -- it would "
"ship unlaunched"])

def test_loader(self):
# The fixture declares opencl opt-in (see CSV), so the loader text has to name it.
text = ('List<String> LIBRARY_BACKENDS = Arrays.asList("metal", "cpu");\n'
'List<String> MODULE_BACKENDS = Arrays.asList(\n "cuda13", "vulkan",\n "opencl");')
'List<String> MODULE_BACKENDS = Arrays.asList(\n "cuda13", "vulkan",\n "opencl");\n'
'Set<String> OPT_IN_BACKENDS = Collections.singleton("opencl");')
self.assertEqual(natives.check_loader(ROWS, text), [])
self.assertEqual(natives.check_loader(ROWS, text.replace('"cuda13", ', "")),
["LlamaLoader.MODULE_BACKENDS does not know 'cuda13' -- its jars would never load"])
self.assertEqual(natives.check_loader(ROWS, text.replace('"opencl"', '"opencl", "msvc"')),
["LlamaLoader.MODULE_BACKENDS names 'msvc', which no module jar of natives.csv ships"])
self.assertEqual(
natives.check_loader(ROWS, text.replace('"opencl");' + chr(10), '"opencl", "msvc");' + chr(10))),
["LlamaLoader.MODULE_BACKENDS names 'msvc', which no module jar of natives.csv ships"])
self.assertEqual(natives.check_loader(ROWS, text.replace('"metal", ', "")),
["LlamaLoader.LIBRARY_BACKENDS does not know 'metal' -- its jars would never load"])

# The opt-in column and the loader constant have to agree in both directions. A backend the
# list calls opt-in while the loader does not would ship a jar that takes over silently --
# the measured OpenVINO regression the column exists for -- and the other way round a jar
# would be inert with no row saying why.
self.assertEqual(natives.check_loader(ROWS, text.replace('Collections.singleton("opencl")', "emptySet()")),
["LlamaLoader.OPT_IN_BACKENDS does not know 'opencl', which natives.csv declares opt-in"])
self.assertEqual(
natives.check_loader(ROWS, text.replace('singleton("opencl")', 'asList("opencl", "vulkan")')),
["LlamaLoader.OPT_IN_BACKENDS names 'vulkan', which no row of natives.csv declares opt-in"])

def test_check_rows_holds_the_optin_column(self):
missing = natives.rows("classifier,directory,library,platform,kind,optin\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library\n")
self.assertTrue(any("optin must be yes or no" in f for f in natives.check_rows(missing)))

# Only a module can be opt-in: a library jar is the one jar that has to load.
library = natives.rows("classifier,directory,library,platform,kind,optin\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,yes\n")
self.assertTrue(any("only a module can be opt-in" in f for f in natives.check_rows(library)))

# Opt-in is decided per backend, so two jars of one backend cannot disagree -- one of them
# would be inert and the other not, which no reader would expect.
split = natives.rows("classifier,directory,library,platform,kind,optin\n"
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no\n"
"cpu-windows-x86-64,Windows/x86_64/cpu,jllama.dll,yes,library,no\n"
"openvino-linux-x86-64,Linux/x86_64/openvino,libggml-openvino.so,no,module,yes\n"
"openvino-windows-x86-64,Windows/x86_64/openvino,ggml-openvino.dll,no,module,no\n")
self.assertTrue(any("is opt-in in one row and not in another" in f for f in natives.check_rows(split)))

def test_cmake_both_directions(self):
text = "\n".join(f" set(JLLAMA_BACKEND {b})" for b in ("cuda13", "cpu", "vulkan", "opencl", "metal"))
self.assertEqual(natives.check_cmake(ROWS, text), [])
Expand Down
48 changes: 24 additions & 24 deletions .github/natives.csv
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,27 @@
# a build job uploading natives-<classifier>, and, for a new backend, its name in CMakeLists.txt
# and in LlamaLoader.LIBRARY_BACKENDS or MODULE_BACKENDS. .github/check-natives.py fails until all
# of them agree.
classifier,directory,library,platform,kind
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library
cpu-linux-aarch64,Linux/aarch64/cpu,libjllama.so,yes,library
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library
cpu-android-x86-64,Linux-Android/x86_64/cpu,libjllama.so,no,library
cpu-windows-x86-64,Windows/x86_64/cpu,jllama.dll,yes,library
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module
cuda13-windows-x86-64,Windows/x86_64/cuda13,ggml-cuda.dll,no,module
rocm-linux-x86-64,Linux/x86_64/rocm,libggml-hip.so,no,module
rocm-windows-x86-64,Windows/x86_64/rocm,ggml-hip.dll,no,module
sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module
sycl-windows-x86-64,Windows/x86_64/sycl,ggml-sycl.dll,no,module
vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module
vulkan-linux-x86-64,Linux/x86_64/vulkan,libggml-vulkan.so,no,module
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module
vulkan-windows-x86-64,Windows/x86_64/vulkan,ggml-vulkan.dll,no,module
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module
opencl-windows-aarch64,Windows/aarch64/opencl,ggml-opencl.dll,no,module
opencl-windows-x86-64,Windows/x86_64/opencl,ggml-opencl.dll,no,module
openvino-linux-x86-64,Linux/x86_64/openvino,libggml-openvino.so,no,module
openvino-windows-x86-64,Windows/x86_64/openvino,ggml-openvino.dll,no,module
classifier,directory,library,platform,kind,optin
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no
cpu-linux-aarch64,Linux/aarch64/cpu,libjllama.so,yes,library,no
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library,no
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library,no
cpu-android-x86-64,Linux-Android/x86_64/cpu,libjllama.so,no,library,no
cpu-windows-x86-64,Windows/x86_64/cpu,jllama.dll,yes,library,no
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library,no
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library,no
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module,no
cuda13-windows-x86-64,Windows/x86_64/cuda13,ggml-cuda.dll,no,module,no
rocm-linux-x86-64,Linux/x86_64/rocm,libggml-hip.so,no,module,no
rocm-windows-x86-64,Windows/x86_64/rocm,ggml-hip.dll,no,module,no
sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module,no
sycl-windows-x86-64,Windows/x86_64/sycl,ggml-sycl.dll,no,module,no
vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module,no
vulkan-linux-x86-64,Linux/x86_64/vulkan,libggml-vulkan.so,no,module,no
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module,no
vulkan-windows-x86-64,Windows/x86_64/vulkan,ggml-vulkan.dll,no,module,no
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module,no
opencl-windows-aarch64,Windows/aarch64/opencl,ggml-opencl.dll,no,module,no
opencl-windows-x86-64,Windows/x86_64/opencl,ggml-opencl.dll,no,module,no
openvino-linux-x86-64,Linux/x86_64/openvino,libggml-openvino.so,no,module,yes
openvino-windows-x86-64,Windows/x86_64/openvino,ggml-openvino.dll,no,module,yes
7 changes: 6 additions & 1 deletion .github/smoke-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ smoke_classpath() {
# The GPU module backends whose jars are in the smoke set (from .github/natives.csv), one per line.
smoke_set_modules() {
local set_dir="$1"
grep -v -e '^#' -e '^classifier,' -e '^$' "$SMOKE_LIST" | tr -d '\r' | awk -F, '$5 == "module" { print $1 "," $2 }' \
# $6 == "yes" is an opt-in module (natives.csv): its jar on the classpath does nothing until
# net.ladenthin.llama.backend names it, so the loader deliberately leaves it out and the line
# below must not expect it. smoke-natives-jars.sh names them instead, which is where the opt-in
# path itself is exercised.
grep -v -e '^#' -e '^classifier,' -e '^$' "$SMOKE_LIST" | tr -d '\r' \
| awk -F, '$5 == "module" && $6 != "yes" { print $1 "," $2 }' \
| while IFS=, read -r classifier dir; do
if find "$set_dir" -maxdepth 1 -name "llama-*-$classifier.jar" | grep -q .; then
basename "$dir"
Expand Down
15 changes: 13 additions & 2 deletions .github/smoke-natives-jars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ case "$(uname -s)-$(uname -m)" in
esac
expected_modules="$(grep -v -e '^#' -e '^classifier,' -e '^$' "$LIST" | tr -d '\r' \
| awk -F, -v t="$tree/" 'index($2, t) == 1 && $5 == "module" { sub(t, "", $2); print $2 }')"
# Every module of the platform, opt-in ones included, named through the property. This is the only
# place in CI that loads an opt-in module at all: the loader leaves it out by default (that is the
# point of the column), and the model-backed smoke rows must not have it, since OpenVINO's CPU
# plugin would take over and run 172x slower. Here nothing is inferred -- the library is loaded and
# the process exits -- so naming them proves both that the jars coexist and that the property
# enables what it names.
backend_property=""
if [ -n "$expected_modules" ]; then
backend_property="-Dnet.ladenthin.llama.backend=$(tr '\n' ',' <<< "$expected_modules" | sed 's/,$//')"
echo "enabling every module of $tree: $backend_property"
fi

# The classes jar is the one without a classifier; every other jar but sources/javadoc/the fat jar
# is a natives jar.
Expand Down Expand Up @@ -64,5 +75,5 @@ run() {
echo "OK ($label): $backend; $(grep 'native load smoke OK' <<< "$out")"
}

run "classpath" -cp "$path"
run "module path" --module-path "$path" --add-modules ALL-MODULE-PATH
run "classpath" ${backend_property:+"$backend_property"} -cp "$path"
run "module path" ${backend_property:+"$backend_property"} --module-path "$path" --add-modules ALL-MODULE-PATH
Loading
Loading