Skip to content

Commit f2260e8

Browse files
Merge pull request #527 from bernardladenthin/local/optin-column
Declare the opt-in modules in natives.csv, so the smokes know what the loader leaves out
2 parents 58ba961 + 691faed commit f2260e8

8 files changed

Lines changed: 166 additions & 57 deletions

File tree

‎.github/buildcheck/natives.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
uploads a smoke set llama-smoke-<target> per smoke target, and every smoke
1313
target is launched by a job that downloads its set
1414
* LlamaLoader LIBRARY_BACKENDS / MODULE_BACKENDS know every backend of their kind (else a
15-
jar ships and never loads)
15+
jar ships and never loads), and OPT_IN_BACKENDS names exactly the backends
16+
the optin column declares (else an opt-in jar takes over silently, or an
17+
inert one has no row saying why)
1618
* CMakeLists.txt names exactly the backend directories of the list
1719
* nativedeps.ALLOWED holds an allowlist for every library directory (cpu, metal), and for no
1820
directory the list lacks
@@ -137,6 +139,11 @@ def check_rows(natives):
137139
if not r["classifier"].startswith(backend(r) + "-") or r["platform"] not in ("yes", "no"):
138140
failures.append(f"natives.csv: row {r['classifier']} -- classifier must start with its "
139141
f"directory's backend '{backend(r)}', platform must be yes or no")
142+
if r.get("optin") not in ("yes", "no"):
143+
failures.append(f"natives.csv: row {r['classifier']} -- optin must be yes or no")
144+
elif r["optin"] == "yes" and r.get("kind") != "module":
145+
failures.append(f"natives.csv: row {r['classifier']} -- only a module can be opt-in: a library jar "
146+
f"is the one jar that must load")
140147
if r.get("kind") not in KINDS:
141148
failures.append(f"natives.csv: row {r['classifier']} -- kind must be one of {KINDS}")
142149
elif r["kind"] == "library" and not re.fullmatch(r"(lib)?jllama\.(so|dll|dylib)", r["library"]):
@@ -150,6 +157,11 @@ def check_rows(natives):
150157
failures.append(f"natives.csv: row {r['classifier']} -- a module cannot be a platform jar: it "
151158
f"holds no library")
152159
trees.setdefault(tree(r), []).append(r)
160+
for b in sorted({backend(r) for r in natives}):
161+
answers = {r.get("optin") for r in natives if backend(r) == b}
162+
if len(answers) > 1:
163+
failures.append(f"natives.csv: backend '{b}' is opt-in in one row and not in another "
164+
f"({sorted(answers)}) -- the loader decides per backend, so the rows cannot differ")
153165
for t, group in sorted(trees.items()):
154166
if not any(r.get("kind") == "library" for r in group):
155167
failures.append(f"natives.csv: {t} has module jars but no library jar -- nothing would load them")
@@ -238,6 +250,16 @@ def constant(name):
238250
block = re.search(name + r"\s*=(.*?);", loader_text, re.S)
239251
return set(re.findall(r'"([^"]+)"', block.group(1))) if block else set()
240252
failures = []
253+
# OPT_IN_BACKENDS is what makes a module jar inert until the property names it, so a backend the
254+
# list declares opt-in while the loader does not would ship a jar that takes over silently --
255+
# which is the measured OpenVINO regression this column exists for -- and the other way round a
256+
# jar would never load with no row saying why.
257+
listed_optin = {backend(r) for r in natives if r.get("optin") == "yes"}
258+
known_optin = constant("OPT_IN_BACKENDS")
259+
failures += [f"LlamaLoader.OPT_IN_BACKENDS does not know '{b}', which natives.csv declares opt-in"
260+
for b in sorted(listed_optin - known_optin)]
261+
failures += [f"LlamaLoader.OPT_IN_BACKENDS names '{b}', which no row of natives.csv declares opt-in"
262+
for b in sorted(known_optin - listed_optin)]
241263
for kind, const in (("library", "LIBRARY_BACKENDS"), ("module", "MODULE_BACKENDS")):
242264
listed = {backend(r) for r in natives if r["kind"] == kind}
243265
known = constant(const)

‎.github/buildcheck/tests/test_natives.py‎

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
from buildcheck.tests.helpers import REPO, upload, workflow_text
1212

1313
CSV = """# a comment
14-
classifier,directory,library,platform,kind
15-
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library
16-
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module
17-
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library
18-
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library
19-
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module
20-
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library
21-
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module
22-
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library
14+
classifier,directory,library,platform,kind,optin
15+
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no
16+
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module,no
17+
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library,no
18+
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library,no
19+
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module,no
20+
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library,no
21+
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module,yes
22+
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library,no
2323
"""
2424

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

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

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

104105
def test_check_rows_refuses_a_module_without_a_library_and_colliding_file_names(self):
105-
orphan = natives.rows("classifier,directory,library,platform,kind\n"
106-
"vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module\n")
106+
orphan = natives.rows("classifier,directory,library,platform,kind,optin\n"
107+
"vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module,no\n")
107108
self.assertEqual(natives.check_rows(orphan),
108109
["natives.csv: Linux/aarch64 has module jars but no library jar -- nothing would load them"])
109-
twice = natives.rows("classifier,directory,library,platform,kind\n"
110-
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library\n"
111-
"sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module\n"
112-
"sycl-fp32-linux-x86-64,Linux/x86_64/sycl-fp32,libggml-sycl.so,no,module\n")
110+
twice = natives.rows("classifier,directory,library,platform,kind,optin\n"
111+
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no\n"
112+
"sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module,no\n"
113+
"sycl-fp32-linux-x86-64,Linux/x86_64/sycl-fp32,libggml-sycl.so,no,module,no\n")
113114
failures = natives.check_rows(twice)
114115
self.assertEqual(len(failures), 1, failures)
115116
self.assertIn("same name", failures[0])
116-
platform_module = natives.rows("classifier,directory,library,platform,kind\n"
117-
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library\n"
118-
"cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,yes,module\n")
117+
platform_module = natives.rows("classifier,directory,library,platform,kind,optin\n"
118+
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no\n"
119+
"cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,yes,module,no\n")
119120
self.assertIn("cannot be a platform jar", natives.check_rows(platform_module)[0])
120121

121122

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

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

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

193+
# The opt-in column and the loader constant have to agree in both directions. A backend the
194+
# list calls opt-in while the loader does not would ship a jar that takes over silently --
195+
# the measured OpenVINO regression the column exists for -- and the other way round a jar
196+
# would be inert with no row saying why.
197+
self.assertEqual(natives.check_loader(ROWS, text.replace('Collections.singleton("opencl")', "emptySet()")),
198+
["LlamaLoader.OPT_IN_BACKENDS does not know 'opencl', which natives.csv declares opt-in"])
199+
self.assertEqual(
200+
natives.check_loader(ROWS, text.replace('singleton("opencl")', 'asList("opencl", "vulkan")')),
201+
["LlamaLoader.OPT_IN_BACKENDS names 'vulkan', which no row of natives.csv declares opt-in"])
202+
203+
def test_check_rows_holds_the_optin_column(self):
204+
missing = natives.rows("classifier,directory,library,platform,kind,optin\n"
205+
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library\n")
206+
self.assertTrue(any("optin must be yes or no" in f for f in natives.check_rows(missing)))
207+
208+
# Only a module can be opt-in: a library jar is the one jar that has to load.
209+
library = natives.rows("classifier,directory,library,platform,kind,optin\n"
210+
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,yes\n")
211+
self.assertTrue(any("only a module can be opt-in" in f for f in natives.check_rows(library)))
212+
213+
# Opt-in is decided per backend, so two jars of one backend cannot disagree -- one of them
214+
# would be inert and the other not, which no reader would expect.
215+
split = natives.rows("classifier,directory,library,platform,kind,optin\n"
216+
"cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no\n"
217+
"cpu-windows-x86-64,Windows/x86_64/cpu,jllama.dll,yes,library,no\n"
218+
"openvino-linux-x86-64,Linux/x86_64/openvino,libggml-openvino.so,no,module,yes\n"
219+
"openvino-windows-x86-64,Windows/x86_64/openvino,ggml-openvino.dll,no,module,no\n")
220+
self.assertTrue(any("is opt-in in one row and not in another" in f for f in natives.check_rows(split)))
221+
189222
def test_cmake_both_directions(self):
190223
text = "\n".join(f" set(JLLAMA_BACKEND {b})" for b in ("cuda13", "cpu", "vulkan", "opencl", "metal"))
191224
self.assertEqual(natives.check_cmake(ROWS, text), [])

‎.github/natives.csv‎

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,27 @@
1919
# a build job uploading natives-<classifier>, and, for a new backend, its name in CMakeLists.txt
2020
# and in LlamaLoader.LIBRARY_BACKENDS or MODULE_BACKENDS. .github/check-natives.py fails until all
2121
# of them agree.
22-
classifier,directory,library,platform,kind
23-
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library
24-
cpu-linux-aarch64,Linux/aarch64/cpu,libjllama.so,yes,library
25-
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library
26-
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library
27-
cpu-android-x86-64,Linux-Android/x86_64/cpu,libjllama.so,no,library
28-
cpu-windows-x86-64,Windows/x86_64/cpu,jllama.dll,yes,library
29-
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library
30-
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library
31-
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module
32-
cuda13-windows-x86-64,Windows/x86_64/cuda13,ggml-cuda.dll,no,module
33-
rocm-linux-x86-64,Linux/x86_64/rocm,libggml-hip.so,no,module
34-
rocm-windows-x86-64,Windows/x86_64/rocm,ggml-hip.dll,no,module
35-
sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module
36-
sycl-windows-x86-64,Windows/x86_64/sycl,ggml-sycl.dll,no,module
37-
vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module
38-
vulkan-linux-x86-64,Linux/x86_64/vulkan,libggml-vulkan.so,no,module
39-
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module
40-
vulkan-windows-x86-64,Windows/x86_64/vulkan,ggml-vulkan.dll,no,module
41-
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module
42-
opencl-windows-aarch64,Windows/aarch64/opencl,ggml-opencl.dll,no,module
43-
opencl-windows-x86-64,Windows/x86_64/opencl,ggml-opencl.dll,no,module
44-
openvino-linux-x86-64,Linux/x86_64/openvino,libggml-openvino.so,no,module
45-
openvino-windows-x86-64,Windows/x86_64/openvino,ggml-openvino.dll,no,module
22+
classifier,directory,library,platform,kind,optin
23+
cpu-linux-x86-64,Linux/x86_64/cpu,libjllama.so,yes,library,no
24+
cpu-linux-aarch64,Linux/aarch64/cpu,libjllama.so,yes,library,no
25+
cpu-linux-s390x,Linux/s390x/cpu,libjllama.so,yes,library,no
26+
cpu-android-aarch64,Linux-Android/aarch64/cpu,libjllama.so,no,library,no
27+
cpu-android-x86-64,Linux-Android/x86_64/cpu,libjllama.so,no,library,no
28+
cpu-windows-x86-64,Windows/x86_64/cpu,jllama.dll,yes,library,no
29+
cpu-windows-aarch64,Windows/aarch64/cpu,jllama.dll,yes,library,no
30+
metal-macos-aarch64,Mac/aarch64/metal,libjllama.dylib,yes,library,no
31+
cuda13-linux-x86-64,Linux/x86_64/cuda13,libggml-cuda.so,no,module,no
32+
cuda13-windows-x86-64,Windows/x86_64/cuda13,ggml-cuda.dll,no,module,no
33+
rocm-linux-x86-64,Linux/x86_64/rocm,libggml-hip.so,no,module,no
34+
rocm-windows-x86-64,Windows/x86_64/rocm,ggml-hip.dll,no,module,no
35+
sycl-linux-x86-64,Linux/x86_64/sycl,libggml-sycl.so,no,module,no
36+
sycl-windows-x86-64,Windows/x86_64/sycl,ggml-sycl.dll,no,module,no
37+
vulkan-linux-aarch64,Linux/aarch64/vulkan,libggml-vulkan.so,no,module,no
38+
vulkan-linux-x86-64,Linux/x86_64/vulkan,libggml-vulkan.so,no,module,no
39+
vulkan-windows-aarch64,Windows/aarch64/vulkan,ggml-vulkan.dll,no,module,no
40+
vulkan-windows-x86-64,Windows/x86_64/vulkan,ggml-vulkan.dll,no,module,no
41+
opencl-android-aarch64,Linux-Android/aarch64/opencl,libggml-opencl.so,no,module,no
42+
opencl-windows-aarch64,Windows/aarch64/opencl,ggml-opencl.dll,no,module,no
43+
opencl-windows-x86-64,Windows/x86_64/opencl,ggml-opencl.dll,no,module,no
44+
openvino-linux-x86-64,Linux/x86_64/openvino,libggml-openvino.so,no,module,yes
45+
openvino-windows-x86-64,Windows/x86_64/openvino,ggml-openvino.dll,no,module,yes

‎.github/smoke-lib.sh‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ smoke_classpath() {
2323
# The GPU module backends whose jars are in the smoke set (from .github/natives.csv), one per line.
2424
smoke_set_modules() {
2525
local set_dir="$1"
26-
grep -v -e '^#' -e '^classifier,' -e '^$' "$SMOKE_LIST" | tr -d '\r' | awk -F, '$5 == "module" { print $1 "," $2 }' \
26+
# $6 == "yes" is an opt-in module (natives.csv): its jar on the classpath does nothing until
27+
# net.ladenthin.llama.backend names it, so the loader deliberately leaves it out and the line
28+
# below must not expect it. smoke-natives-jars.sh names them instead, which is where the opt-in
29+
# path itself is exercised.
30+
grep -v -e '^#' -e '^classifier,' -e '^$' "$SMOKE_LIST" | tr -d '\r' \
31+
| awk -F, '$5 == "module" && $6 != "yes" { print $1 "," $2 }' \
2732
| while IFS=, read -r classifier dir; do
2833
if find "$set_dir" -maxdepth 1 -name "llama-*-$classifier.jar" | grep -q .; then
2934
basename "$dir"

‎.github/smoke-natives-jars.sh‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ case "$(uname -s)-$(uname -m)" in
3535
esac
3636
expected_modules="$(grep -v -e '^#' -e '^classifier,' -e '^$' "$LIST" | tr -d '\r' \
3737
| awk -F, -v t="$tree/" 'index($2, t) == 1 && $5 == "module" { sub(t, "", $2); print $2 }')"
38+
# Every module of the platform, opt-in ones included, named through the property. This is the only
39+
# place in CI that loads an opt-in module at all: the loader leaves it out by default (that is the
40+
# point of the column), and the model-backed smoke rows must not have it, since OpenVINO's CPU
41+
# plugin would take over and run 172x slower. Here nothing is inferred -- the library is loaded and
42+
# the process exits -- so naming them proves both that the jars coexist and that the property
43+
# enables what it names.
44+
backend_property=""
45+
if [ -n "$expected_modules" ]; then
46+
backend_property="-Dnet.ladenthin.llama.backend=$(tr '\n' ',' <<< "$expected_modules" | sed 's/,$//')"
47+
echo "enabling every module of $tree: $backend_property"
48+
fi
3849

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

67-
run "classpath" -cp "$path"
68-
run "module path" --module-path "$path" --add-modules ALL-MODULE-PATH
78+
run "classpath" ${backend_property:+"$backend_property"} -cp "$path"
79+
run "module path" ${backend_property:+"$backend_property"} --module-path "$path" --add-modules ALL-MODULE-PATH

0 commit comments

Comments
 (0)