Skip to content

Commit bddf6a0

Browse files
committed
Merge main (llama.cpp b11901, TTS speaker reference) into the MoE branch
One conflict, CLAUDE.md's counters: 39 native methods (34 LlamaModel + 4 TextToSpeech + 1 LlamaQuantizer), 49 JNI entry points (jllama.cpp 39), test_c_api.cpp 55 tests with five CApiStatic cases, 681 test definitions and 725 ctest cases. All 13 patches, 0022 included, apply at b11901; upstream did not change repack.cpp, ggml-cpu.h or ggml-cpu.cpp between b11887 and b11901. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WkDJfaG3Bw1uyHkEhinjqj
2 parents 7f9ca6a + 1fee05d commit bddf6a0

39 files changed

Lines changed: 980 additions & 105 deletions

‎.github/buildcheck/nativedeps.py‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
and the directories in GLIBC_CEILING, built in a manylinux image or against its sysroot, may not
3636
reference a glibc symbol version above the floor they promise, nor a libstdc++ one (GLIBCXX_*,
3737
CXXABI_*) above LIBSTDCXX_CEILING. Android libraries must also have every LOAD segment 16 KB
38-
aligned.
38+
aligned, and on s390x every typeinfo name (_ZTS*) of the static symbol table must sit at the even
39+
address the ABI promises (TYPEINFO_NAME_ALIGNMENT; a jllama library without any is not examined,
40+
which fails too).
3941
That every listed build arrived is merge-native-artifacts.sh's check.
4042
4143
Exit codes: 0 clean, 1 violation, 2 nothing found to check.
@@ -262,6 +264,72 @@ def elf_exports(data):
262264
return out
263265

264266

267+
SHT_SYMTAB = 2
268+
SHN_LORESERVE = 0xFF00
269+
270+
271+
def elf_symbols(data):
272+
"""(name, address) of every defined symbol of the static symbol table (.symtab), whatever its
273+
binding: the export list and -fvisibility=hidden make almost every symbol of a shipped library
274+
local there. A stripped library has no .symtab, and the list is empty."""
275+
is64, end, sections = _elf_sections(data)
276+
out = []
277+
for sh_type, sh_offset, sh_size, sh_link, _ in sections:
278+
if sh_type != SHT_SYMTAB:
279+
continue
280+
strtab = sections[sh_link][1]
281+
entry = 24 if is64 else 16
282+
for off in range(sh_offset, sh_offset + sh_size, entry):
283+
if is64:
284+
st_name, _, _, st_shndx, st_value = struct.unpack_from(end + "IBBHQ", data, off)
285+
else:
286+
st_name, st_value, _, _, _, st_shndx = struct.unpack_from(end + "IIIBBH", data, off)
287+
if st_name == 0 or st_shndx == 0 or st_shndx >= SHN_LORESERVE:
288+
continue
289+
out.append((_cstring(data, strtab + st_name), st_value))
290+
return out
291+
292+
293+
# The s390x ELF ABI gives every global symbol an even address, and GCC relies on it: it loads an
294+
# address with LARL, whose R_390_PC32DBL relocation counts in halfwords. clang emits typeinfo name
295+
# strings (_ZTS*) with the alignment of char (ItaniumCXXABI.cpp, GetAddrOfTypeName), and the
296+
# s390x build mixes clang's objects with GCC's -- gcc-toolset-14's libstdc++_nonshared.a, which
297+
# loads _ZTSSt19_Sp_make_shared_tag with LARL. Whether a link then fails ("misaligned symbol")
298+
# depends on where the copy it keeps lands: green on main, red in the next PR's jllama_test link
299+
# (#577). .github/manylinux/s390x-typeinfo-names.ld (SUBALIGN(2) over every .rodata._ZTS*
300+
# section) and -fdata-sections (which gives every typeinfo name such a section, the ones of
301+
# classes with a key function included) make it hold for all of them; this check is what notices
302+
# when they stop being in effect, in every build rather than in the one whose layout comes out odd.
303+
TYPEINFO_NAME_ALIGNMENT = {"s390x": 2}
304+
305+
306+
def typeinfo_name_alignment(rel):
307+
"""The alignment the ABI of the directory of `rel` (.../<OS>/<ARCH>/<backend>/<library>)
308+
requires for typeinfo names, or None."""
309+
parts = rel.split("/")
310+
return TYPEINFO_NAME_ALIGNMENT.get(parts[-3]) if len(parts) >= 4 else None
311+
312+
313+
def typeinfo_name_violations(rel, symbols, alignment, required=True):
314+
"""Typeinfo names (_ZTS*) among `symbols` ((name, address) pairs) at an address that is not a
315+
multiple of `alignment`. A library that must carry them (`required`) and carries none was not
316+
examined -- stripped, or its table unreadable -- which is never a pass."""
317+
names = [(n, v) for n, v in symbols if n.startswith("_ZTS")]
318+
if not names:
319+
if not required:
320+
return []
321+
return [f"{rel}: no typeinfo name (_ZTS*) in its symbol table ({len(symbols)} symbols) -- stripped, or the "
322+
f"table could not be read, so their {alignment}-byte alignment cannot be checked"]
323+
odd = [n for n, v in names if v % alignment]
324+
if not odd:
325+
return []
326+
shown = ", ".join(odd[:8]) + (f", ... ({len(odd)} in all)" if len(odd) > 8 else "")
327+
return [f"{rel}: {len(odd)} of {len(names)} typeinfo names (_ZTS*) are not {alignment}-byte aligned: {shown} -- "
328+
f"GCC code (gcc-toolset's libstdc++_nonshared.a) loads them with LARL, so a link that keeps such a "
329+
f"copy fails with 'misaligned symbol'; .github/manylinux/s390x-typeinfo-names.ld or -fdata-sections "
330+
f"(s390x-toolchain.cmake) is not in effect"]
331+
332+
265333
def elf_load_alignments(data):
266334
"""p_align of every PT_LOAD program header."""
267335
if data[:4] != b"\x7fELF":
@@ -611,6 +679,7 @@ def main(argv):
611679
rel = os.path.relpath(path, root).replace(os.sep, "/")
612680
siblings = {n for n in os.listdir(os.path.dirname(path)) if n != os.path.basename(path)}
613681
alignments, runpath, glibc, glibcxx, cxxabi, note = (), None, None, None, None, ""
682+
typeinfo = []
614683
try:
615684
if path.endswith(".so"):
616685
with open(path, "rb") as f:
@@ -620,6 +689,10 @@ def main(argv):
620689
glibcxx, cxxabi = info["glibcxx"], info["cxxabi"]
621690
if "/Linux-Android/" in "/" + rel:
622691
alignments = elf_load_alignments(data)
692+
symbol_alignment = typeinfo_name_alignment(rel)
693+
if symbol_alignment:
694+
typeinfo = typeinfo_name_violations(rel, elf_symbols(data), symbol_alignment,
695+
required=os.path.basename(path) in JLLAMA_LIBRARIES)
623696
note = (f" [runpath={runpath}]" if runpath else "") + "".join(
624697
f" [{glibc_name(v, family)}]" for family, v in (("GLIBC", glibc), ("GLIBCXX", glibcxx),
625698
("CXXABI", cxxabi)) if v)
@@ -630,6 +703,7 @@ def main(argv):
630703
continue
631704
print(f"{rel}: {' '.join(deps)}{note}")
632705
failures += violations(rel, deps, alignments, siblings, runpath, glibc, glibcxx, cxxabi)
706+
failures += typeinfo
633707
if os.path.basename(path) in JLLAMA_LIBRARIES:
634708
try:
635709
names = exports(path)

‎.github/buildcheck/tests/test_nativedeps.py‎

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
from buildcheck import nativedeps
1313

1414

15-
def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()):
15+
def elf64(needed, runpath=None, glibc=(), exports=(), undefined=(), symtab=()):
1616
"""A minimal little-endian ELF64 with a .dynstr and a .dynamic naming `needed` (and a
1717
DT_RUNPATH when given), plus a .gnu.version_r requiring the `glibc` symbol versions, plus a
18-
.dynsym defining `exports` (global) and referencing `undefined`."""
18+
.dynsym defining `exports` (global) and referencing `undefined`, plus a .symtab defining the
19+
`symtab` (name, address) pairs as local objects."""
1920
strtab = b"\0"
2021
offsets = {}
2122
for name in (list(needed) + ([runpath] if runpath is not None else []) + (["libc.so.6"] if glibc else [])
22-
+ list(glibc) + list(exports) + list(undefined)):
23+
+ list(glibc) + list(exports) + list(undefined) + [n for n, _ in symtab]):
2324
offsets[name] = len(strtab)
2425
strtab += name.encode() + b"\0"
2526
dynamic = b"".join(struct.pack("<qQ", 1, offsets[n]) for n in needed)
@@ -36,16 +37,20 @@ def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()):
3637
dynsym += struct.pack("<IBBHQQ", offsets[name], (1 << 4) | 2, 0, 1, 0x1000, 8)
3738
for name in undefined: # SHN_UNDEF: a reference, not an export
3839
dynsym += struct.pack("<IBBHQQ", offsets[name], (1 << 4) | 2, 0, 0, 0, 0)
40+
statics = bytes(24) # the null symbol
41+
for name, address in symtab: # STB_LOCAL (0) << 4 | STT_OBJECT (1), defined in section 1
42+
statics += struct.pack("<IBBHQQ", offsets[name], 1, 0, 1, address, 8)
3943
header_size, section_size = 64, 64
4044
strtab_off = header_size
4145
dynamic_off = strtab_off + len(strtab)
4246
verneed_off = dynamic_off + len(dynamic)
4347
dynsym_off = verneed_off + len(verneed)
44-
shoff = dynsym_off + len(dynsym)
48+
symtab_off = dynsym_off + len(dynsym)
49+
shoff = symtab_off + (len(statics) if symtab else 0)
4550
header = bytearray(header_size)
4651
header[:6] = b"\x7fELF\x02\x01"
4752
struct.pack_into("<Q", header, 0x28, shoff)
48-
nsections = 3 + (1 if glibc else 0) + (1 if exports or undefined else 0)
53+
nsections = 3 + (1 if glibc else 0) + (1 if exports or undefined else 0) + (1 if symtab else 0)
4954
struct.pack_into("<HH", header, 0x3A, section_size, nsections)
5055
sections = bytes(section_size) # SHT_NULL
5156
sections += struct.pack("<IIQQQQIIQQ", 0, 3, 0, 0, strtab_off, len(strtab), 0, 0, 1, 0) # SHT_STRTAB
@@ -54,7 +59,47 @@ def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()):
5459
sections += struct.pack("<IIQQQQIIQQ", 0, 0x6FFFFFFE, 0, 0, verneed_off, len(verneed), 1, 1, 4, 0)
5560
if exports or undefined:
5661
sections += struct.pack("<IIQQQQIIQQ", 0, 11, 0, 0, dynsym_off, len(dynsym), 1, 1, 8, 24) # SHT_DYNSYM
57-
return bytes(header) + strtab + dynamic + verneed + dynsym + sections
62+
if symtab:
63+
sections += struct.pack("<IIQQQQIIQQ", 0, 2, 0, 0, symtab_off, len(statics), 1, 1, 8, 24) # SHT_SYMTAB
64+
return (bytes(header) + strtab + dynamic + verneed + dynsym + (statics if symtab else b"") + sections)
65+
66+
67+
def elf_symtab(symbols, big_endian=False, is64=True):
68+
"""A minimal ELF with only a .strtab and a .symtab: the (name, address, shndx) triples as local
69+
objects -- in either byte order and class, s390x being big-endian ELF64."""
70+
end = ">" if big_endian else "<"
71+
strtab = b"\0"
72+
offsets = {}
73+
for name, _, _ in symbols:
74+
offsets[name] = len(strtab)
75+
strtab += name.encode() + b"\0"
76+
entry = 24 if is64 else 16
77+
table = bytes(entry)
78+
for name, address, shndx in symbols:
79+
if is64:
80+
table += struct.pack(end + "IBBHQQ", offsets[name], 1, 0, shndx, address, 1)
81+
else:
82+
table += struct.pack(end + "IIIBBH", offsets[name], address, 1, 1, 0, shndx)
83+
header_size = 64 if is64 else 52
84+
section_size = 64 if is64 else 40
85+
strtab_off = header_size
86+
table_off = strtab_off + len(strtab)
87+
shoff = table_off + len(table)
88+
header = bytearray(header_size)
89+
header[:6] = b"\x7fELF" + bytes([2 if is64 else 1, 2 if big_endian else 1])
90+
if is64:
91+
struct.pack_into(end + "Q", header, 0x28, shoff)
92+
struct.pack_into(end + "HH", header, 0x3A, section_size, 3)
93+
sections = bytes(section_size)
94+
sections += struct.pack(end + "IIQQQQIIQQ", 0, 3, 0, 0, strtab_off, len(strtab), 0, 0, 1, 0)
95+
sections += struct.pack(end + "IIQQQQIIQQ", 0, 2, 0, 0, table_off, len(table), 1, 1, 8, entry)
96+
else:
97+
struct.pack_into(end + "I", header, 0x20, shoff)
98+
struct.pack_into(end + "HH", header, 0x2E, section_size, 3)
99+
sections = bytes(section_size)
100+
sections += struct.pack(end + "IIIIIIIIII", 0, 3, 0, 0, strtab_off, len(strtab), 0, 0, 1, 0)
101+
sections += struct.pack(end + "IIIIIIIIII", 0, 2, 0, 0, table_off, len(table), 1, 1, 4, entry)
102+
return bytes(header) + strtab + table + sections
58103

59104

60105
def elf64_segments(alignments):
@@ -455,6 +500,68 @@ def test_the_runtimes_the_jvm_itself_needs_stay_allowed(self):
455500
self.assertIn("libgcc_s.so.1", nativedeps.ALLOWED[directory], directory)
456501

457502

503+
class TypeinfoNameAlignmentTest(unittest.TestCase):
504+
"""s390x: every typeinfo name (_ZTS*) at an even address, the ABI's promise GCC's LARL relies on
505+
(.github/manylinux/s390x-typeinfo-names.ld)."""
506+
507+
def test_elf_symbols_reads_the_static_table_in_both_byte_orders(self):
508+
symbols = [("_ZTS1X", 0x2001, 5), ("undefined", 0, 0), ("absolute", 0x10, 0xFFF1), ("_ZTSSt9exception", 0x40, 7)]
509+
for big_endian in (False, True):
510+
for is64 in (True, False):
511+
data = elf_symtab(symbols, big_endian, is64)
512+
self.assertEqual(nativedeps.elf_symbols(data), [("_ZTS1X", 0x2001), ("_ZTSSt9exception", 0x40)],
513+
(big_endian, is64))
514+
# the dynamic table is not the static one, and a library without .symtab has no symbols
515+
self.assertEqual(nativedeps.elf_symbols(elf64(["libc.so.6"], exports=["JNI_OnLoad"])), [])
516+
self.assertEqual(nativedeps.elf_symbols(elf64(["libc.so.6"], symtab=[("_ZTS1K", 0x30)])), [("_ZTS1K", 0x30)])
517+
518+
def test_only_s390x_asks_for_it(self):
519+
self.assertEqual(nativedeps.typeinfo_name_alignment("x/Linux/s390x/cpu/libjllama.so"), 2)
520+
for rel in ("x/Linux/x86_64/cpu/libjllama.so", "x/Linux/aarch64/cpu/libjllama.so",
521+
"x/Linux-Android/aarch64/cpu/libjllama.so", "libjllama.so"):
522+
self.assertIsNone(nativedeps.typeinfo_name_alignment(rel), rel)
523+
524+
def test_an_odd_typeinfo_name_is_named_and_an_even_one_passes(self):
525+
rel = "x/Linux/s390x/cpu/libjllama.so"
526+
self.assertEqual(nativedeps.typeinfo_name_violations(rel, [("_ZTS1X", 0x2000), ("odd_but_no_typeinfo", 0x3)], 2), [])
527+
found = nativedeps.typeinfo_name_violations(
528+
rel, [("_ZTSSt19_Sp_make_shared_tag", 0x12419CF), ("_ZTS1X", 0x2000), ("data", 0x1)], 2)
529+
self.assertEqual(len(found), 1, found)
530+
self.assertIn("1 of 2 typeinfo names", found[0])
531+
self.assertIn("_ZTSSt19_Sp_make_shared_tag", found[0])
532+
self.assertIn("s390x-typeinfo-names.ld", found[0])
533+
many = [(f"_ZTS{i}X", 2 * i + 1) for i in range(10)]
534+
self.assertIn("(10 in all)", nativedeps.typeinfo_name_violations(rel, many, 2)[0])
535+
536+
def test_a_library_without_typeinfo_names_is_never_a_pass_where_it_must_have_them(self):
537+
found = nativedeps.typeinfo_name_violations("x/Linux/s390x/cpu/libjllama.so", [("JNI_OnLoad", 0x1000)], 2)
538+
self.assertEqual(len(found), 1, found)
539+
self.assertIn("no typeinfo name", found[0])
540+
self.assertEqual(nativedeps.typeinfo_name_violations("x/Linux/s390x/cpu/libother.so", [], 2, required=False), [])
541+
542+
def _run(self, arch, symtab):
543+
with tempfile.TemporaryDirectory() as root:
544+
d = os.path.join(root, "net/ladenthin/llama/Linux", arch, "cpu")
545+
os.makedirs(d)
546+
with open(os.path.join(d, "libjllama.so"), "wb") as f:
547+
f.write(elf64(["libc.so.6"], exports=["JNI_OnLoad", "Java_a"], symtab=symtab))
548+
out, err = io.StringIO(), io.StringIO()
549+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
550+
rc = nativedeps.main(["x", root])
551+
return rc, err.getvalue()
552+
553+
def test_main_holds_the_s390x_library_to_it_and_no_other(self):
554+
self.assertEqual(self._run("s390x", [("_ZTS1X", 0x2000), ("_ZTSSt9exception", 0x2010)])[0], 0)
555+
rc, err = self._run("s390x", [("_ZTS1X", 0x2000), ("_ZTSSt19_Sp_make_shared_tag", 0x2011)])
556+
self.assertEqual(rc, 1)
557+
self.assertIn("_ZTSSt19_Sp_make_shared_tag", err)
558+
rc, err = self._run("s390x", [])
559+
self.assertEqual(rc, 1, "a stripped s390x library is not examined, which is never a pass")
560+
self.assertIn("no typeinfo name", err)
561+
# x86-64's ABI promises no such thing, and its library is not held to it
562+
self.assertEqual(self._run("x86_64", [("_ZTS1X", 0x2001)])[0], 0)
563+
564+
458565
class MainTest(unittest.TestCase):
459566

460567
def test_checks_a_tree_and_reports_violations(self):
@@ -464,7 +571,7 @@ def test_checks_a_tree_and_reports_violations(self):
464571
os.makedirs(ok)
465572
os.makedirs(bad)
466573
with open(os.path.join(ok, "libjllama.so"), "wb") as f:
467-
f.write(elf64(["libc.so.6"], exports=["JNI_OnLoad", "Java_a"]))
574+
f.write(elf64(["libc.so.6"], exports=["JNI_OnLoad", "Java_a"], symtab=[("_ZTS1X", 0x2000)]))
468575
self.assertEqual(nativedeps.main(["x", root]), 0)
469576
with open(os.path.join(bad, "jllama.dll"), "wb") as f:
470577
f.write(pe64(["kernel32.dll", "libomp140.x86_64.dll"], exports=["JNI_OnLoad", "Java_a"]))

‎.github/manylinux/s390x-toolchain.cmake‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,17 @@ set(CMAKE_C_COMPILER_TARGET s390x-linux-gnu)
6060
set(CMAKE_CXX_COMPILER_TARGET s390x-linux-gnu)
6161

6262
set(_jllama_s390x_gcc "${JLLAMA_S390X_SYSROOT}/opt/rh/gcc-toolset-14/root/usr/lib/gcc/s390x-redhat-linux/14")
63-
set(CMAKE_C_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13")
64-
set(CMAKE_CXX_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13")
63+
# -fdata-sections: every typeinfo name gets a .rodata._ZTS* section of its own, so the linker
64+
# script below reaches all of them -- without it the names of classes with a key function and of
65+
# internal types (lambdas) stay in plain .rodata, 444 of 1833 of them at an odd address (measured).
66+
set(CMAKE_C_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13 -fdata-sections")
67+
set(CMAKE_CXX_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13 -fdata-sections")
68+
# s390x-typeinfo-names.ld: clang aligns typeinfo name strings like char, the GCC code of
69+
# libstdc++_nonshared.a loads them with LARL, which needs an even address (see the script);
70+
# verify-native-deps.py holds every typeinfo name of the library to it.
6571
foreach(_kind EXE SHARED MODULE)
66-
set(CMAKE_${_kind}_LINKER_FLAGS_INIT "--ld-path=/usr/bin/s390x-linux-gnu-ld")
72+
set(CMAKE_${_kind}_LINKER_FLAGS_INIT
73+
"--ld-path=/usr/bin/s390x-linux-gnu-ld -Wl,-T,${CMAKE_CURRENT_LIST_DIR}/s390x-typeinfo-names.ld")
6774
endforeach()
6875

6976
set(CMAKE_CROSSCOMPILING_EMULATOR /usr/bin/env "QEMU_LD_PREFIX=${JLLAMA_S390X_SYSROOT}" /usr/bin/qemu-s390x-static)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
3+
*
4+
* SPDX-License-Identifier: MIT OR Apache-2.0
5+
*/
6+
7+
/*
8+
* Augments GNU ld's default script for the s390x cross build (s390x-toolchain.cmake passes it with
9+
* -T; INSERT keeps the default script in force): every typeinfo name string (_ZTS*) lands 2-byte
10+
* aligned.
11+
*
12+
* The s390x ABI guarantees every global symbol an even address, and GCC relies on it: it loads a
13+
* symbol's address with LARL, whose R_390_PC32DBL relocation counts in halfwords. clang emits the
14+
* typeinfo name strings with the alignment of char instead (ItaniumCXXABI.cpp, GetAddrOfTypeName,
15+
* unchanged on LLVM main in 2026-10), and as COMDAT, so the copy the link keeps is the first one --
16+
* clang's, from our objects. gcc-toolset-14's libstdc++_nonshared.a, which the toolset's libstdc++.so
17+
* script adds to every link, is GCC code: fs_path80.o's _M_get_deleter loads
18+
* _ZTSSt19_Sp_make_shared_tag with LARL, and the link fails with "misaligned symbol ... for
19+
* relocation R_390_PC32DBL" whenever the sizes of the strings before it add up to an odd number. A
20+
* layout lottery: green on main, red on the next change (PR #577, the jllama_test link).
21+
*
22+
* SUBALIGN(2) sets the alignment of every input section of this output section to 2. Only typeinfo
23+
* name strings are matched; they are char arrays read byte-wise, and 2 is all the ABI promises
24+
* for them (GCC emits them with 2). INSERT BEFORE .rodata puts the statement ahead of the default
25+
* .rodata pattern (.rodata.*), so it is the first to match these sections. -fdata-sections (in the
26+
* toolchain) gives every typeinfo name such a section, those of classes with a key function and of
27+
* internal types included, and verify-native-deps.py fails a library with one at an odd address.
28+
*/
29+
SECTIONS
30+
{
31+
.rodata.jllama_typeinfo_names : SUBALIGN(2) { *(.rodata._ZTS*) }
32+
}
33+
INSERT BEFORE .rodata;

0 commit comments

Comments
 (0)