Skip to content

fix(s390x): guard the VXE-only helpers upstream left at file scope - #432

Merged
bernardladenthin merged 1 commit into
mainfrom
claude/s390x-repack-vxe-guard
Sep 11, 2026
Merged

fix(s390x): guard the VXE-only helpers upstream left at file scope#432
bernardladenthin merged 1 commit into
mainfrom
claude/s390x-repack-vxe-guard

Conversation

@bernardladenthin

Copy link
Copy Markdown
Owner

Summary

Build and Test Linux s390x fails to compile on main, with no line of this project's code involved. Seen on run #940 (workflow_dispatch on a56829a).

Why it appears now

Question Answer
Did we change anything? No. The build-linux-s390x job is untouched since 38f00b2, 2026-07-14.
What changed then? Upstream added ggml/src/ggml-cpu/arch/s390/repack.cpp at b10902 (#28667, s390x q4_0 repack). git cat-file -e confirms: absent at b10883, present at b10902 and b10903. ggml-cpu/CMakeLists.txt compiles it unconditionally for s390x.
Why didn't the PRs catch it? Both #430 and #431 were merged before their pipelines reached the s390x job. Run #940 is the first full-matrix run since either landed.

This is the s390x work I flagged in #430's description as "worth knowing for the s390x job" — the flag was right, the conclusion drawn from it ("the range does not touch our layer") was right about behaviour and wrong about the build. That claim is corrected in docs/history/ rather than left to mislead the next reader.

The defect

Every function body in repack.cpp is guarded #if defined(__VXE__) || defined(__VXE2__). Three static inline helpers — vxe_dot_acc, vxe_splat_granule, vxe_fold — sit at file scope between two guarded blocks with no guard of their own, and their signatures name int16x8_t / int8x16_t / int32x4_t, which ggml-cpu-impl.h only typedefs inside that same guard. So a non-VXE s390x build dies before reaching any of the code it is supposed to skip:

repack.cpp:73:15: error: 'int16x8_t' does not name a type; did you mean 'int16_t'?
repack.cpp:77:15: error: 'int8x16_t' does not name a type; did you mean 'int16_t'?
repack.cpp:83:15: error: 'int32x4_t' does not name a type; did you mean 'int32_t'?

patches/0013 wraps the three definitions in the identical guard. All 21 call sites (lines 132–210) are already inside __VXE__ blocks, so nothing else moves — the patch is two added lines.

Why our s390x build is non-VXE — the half that is ours

Not obvious, and it is what made this reachable:

option(GGML_VXE  "ggml: enable vxe"  ${GGML_NATIVE})   # ggml/CMakeLists.txt:181

The VXE default follows GGML_NATIVE. The job passes -DGGML_NATIVE=OFF, which is correct for a cross-build — an x86 build host must not bake -march=native into an s390x artifact — and silently switches VXE off with it. No -mvx -mzvector is passed, __VEC__ stays undefined, and ggml-cpu-impl.h's #if defined(__s390x__) && defined(__VEC__) self-define of __VXE__/__VXE2__ never fires. (__VXE__ is not a GCC predefined macro — verified with -dM -E, no -march from z13 to z16 defines it; ggml defines it itself.)

So this job has always produced a scalar s390x binary. That is exactly right for what it is — a big-endian correctness gate for our own layer (WAV writer, JSON/token/embedding transforms, JNI helpers), not a performance target — and it was simply invisible until a file arrived that does not compile that way. A comment on the build step now says so, so the next person does not "fix" it the wrong way.

Which solution is sensible — measured, not argued

All four run through the real cross toolchain (s390x-linux-gnu-g++), compiling the actual file:

Result
A — today's job flags, unpatched ❌ the three CI errors, reproduced verbatim
B — add -DGGML_VXE=ON strictly worse: the self-define sets __VXE__ and __VXE2__ together as soon as __VEC__ exists, while -march stays at the toolchain default arch11 → three errors become dozens of '__builtin_s390_vec_*' matching variant requires z14 or higher
C-DGGML_VXE=ON plus -march=z15 ✅ compiles — but raises the shipped artifact's hardware floor to z15 and makes the qemu ctest gate depend on VXE2 emulation, for vector kernels this job never uses
D — this patch ✅ compiles under A's flags, and stays green under C's flags

D keeps exactly the configuration that worked through b10883, changes nothing about the shipped artifact, fixes a genuine upstream defect, and does not foreclose a future vector build. B is off the table on evidence, not preference.

Test plan

  • Tested locally

  • Added/updated tests — no new test. build-linux-s390x is the test: the file is compiled only for s390x, so a dropped or stale patch fails loudly at compile time. Noted as such in the patch's CLAUDE.md row.

  • CI passes

  • Reproduction first, as the discipline requires: unpatched + the job's own flags → the three errors above, byte for byte.

  • Then the same compile passing: patched + those flags → exit 0; patched + -mvx -mzvector -march=z15 → exit 0.

  • Applier: fresh rm -rf build && cmake -B build -DBUILD_TESTING=ON, configure clean, stamp at head 481c65f091f74c5e7089dd0a3a1cc6b50cced31e with all ten patch SHA-256 lines. The s390x re-verification above was re-run on the applier's output, not my hand edit.

  • Native x86: full cmake --build --config Release clean; ctest 537/537.

  • JNI: mvn -pl llama clean test -Dtest=NativeLibraryLoadSmokeTest 4/4, 0 skipped.

  • publish.yml still parses (yaml.safe_load).

No .java and no project C++ changed, so spotless / SpotBugs / clang-format have no input.

Related issues

None. Upstream-submittable as "ggml-cpu: guard the VXE-only helpers in the s390x repack path"; not yet filed upstream.

Checklist

  • Code follows project style (mvn spotless:apply run for Java changes) — no Java changed.
  • Documentation updated — new patches/0013 row in CLAUDE.md (mechanism, the GGML_NATIVEGGML_VXE coupling, the measured flag comparison, and the standing drop-check); the b10883–b10902 row in docs/history/llama-cpp-breaking-changes.md corrected in place; an explanatory comment on the s390x build step.
  • No breaking changes — the s390x artifact is byte-for-byte the same configuration as before b10902.
Files changed (4)
File Change
llama/patches/0013-s390x-repack-guard-vxe-only-helpers.patch new — two added lines, one file, two hunks
CLAUDE.md patches-table row for 0013
docs/history/llama-cpp-breaking-changes.md correction to the b10883–b10902 row
.github/workflows/publish.yml comment on the s390x build step

CHANGELOG.md is deliberately untouched: it records consumer-visible behaviour, and this is a build fix for a pin bump that is itself not recorded there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH


Generated by Claude Code

The Linux s390x job has been red since b10902 landed, with nothing in this
project involved. Upstream #28667 added a new source file,
ggml/src/ggml-cpu/arch/s390/repack.cpp (absent at b10883), which
ggml-cpu/CMakeLists.txt compiles unconditionally for s390x. Every function
body in it is guarded #if defined(__VXE__) || defined(__VXE2__), but three
static inline helpers -- vxe_dot_acc, vxe_splat_granule, vxe_fold -- sit at
file scope between two guarded blocks with no guard of their own, and their
signatures name int16x8_t / int8x16_t / int32x4_t, which ggml-cpu-impl.h only
typedefs inside that same guard. A non-VXE s390x build therefore dies with
three "does not name a type" errors before reaching any of the code it is
meant to skip. patches/0013 wraps the three definitions in the identical
guard; all 21 call sites are already inside __VXE__ blocks, so nothing moves.

Why this build is non-VXE is the half that is ours, and it is not obvious:
ggml declares option(GGML_VXE "ggml: enable vxe" ${GGML_NATIVE}), so the VXE
default follows GGML_NATIVE. The job passes -DGGML_NATIVE=OFF, which is
correct for a cross-build -- an x86 host must not bake -march=native into an
s390x artifact -- and silently switches VXE off with it. The job has always
produced a scalar s390x binary, which is right for what it is (a big-endian
correctness gate for our own layer, not a performance target); it was simply
invisible until a file arrived that does not compile that way. A comment on
the build step now says so.

The flag-side alternatives were measured with the real cross toolchain, not
reasoned about. -DGGML_VXE=ON alone is strictly worse: the self-define sets
__VXE__ and __VXE2__ together as soon as __VEC__ exists, while -march stays
at the toolchain default arch11, so three errors become dozens of
"'__builtin_s390_vec_*' matching variant requires z14 or higher". Adding
-march=z15 on top does compile, but raises the shipped artifact's hardware
floor to z15 and makes the qemu ctest gate depend on VXE2 emulation -- a real
trade for vector kernels this job never uses. The patch keeps exactly the
configuration that worked through b10883.

Verified: unpatched + the job's own flags reproduces the three CI errors
verbatim; patched + those flags compiles clean; patched + -mvx -mzvector
-march=z15 also compiles clean, so a future vector build is not foreclosed.
Fresh configure applies all ten patches (stamp head 481c65f0), full Release
build clean, ctest 537/537, NativeLibraryLoadSmokeTest 4/4 after a clean.

The docs/history row for b10883-b10902 carried a claim that this disproves --
"the range does not touch our layer" was right about behaviour and wrong
about the build -- and is corrected in place rather than left to mislead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
@sonarqubecloud

Copy link
Copy Markdown

@bernardladenthin
bernardladenthin merged commit 606ebc2 into main Sep 11, 2026
59 of 66 checks passed
@bernardladenthin
bernardladenthin deleted the claude/s390x-repack-vxe-guard branch September 11, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants