align_corners was not the cause either; fix the rebuild path regardless

I wrote into the docs that our llama.cpp predating b4aa7dd47 ("mtmd : use
align_corners for qwen3vl vision position embedding interpolation") was "the
actual cause" of Qwen3.8-27B describing every image identically. The argument was
a plausible commit plus date arithmetic — 0.3.30 vendors llama.cpp from
2026-06-16, the fix landed 2026-07-21, 0.3.36 vendors 2026-10-01 — and every
observation fitted it. I had not run it.

Tested now: 0.3.36 built from source with the full
-DGGML_CUDA=ON -DGGML_VULKAN=ON -DGGML_RPC=ON set and put into service. The same
two images still return byte-identical answers. The fourth theory joins the other
three, and the doc says so.

What the test buys is that the last candidate is eliminated by experiment rather
than argument. The remaining reading is that the qwen3vl graph does not fit this
model at all — the base is qwen3_5 while the projector declares qwen3vl_merger —
and no version bump addresses that. mmproj stays off the qwen38 cards; vision
goes to gemma-4-31B-heretic, which reads the same images correctly.

The rebuild was worth doing for everything else, and it needed three fixes to
complete at all:

  * build-rpc-server.sh asked for the `rpc-server` CMake target, renamed
    upstream to `ggml-rpc-server`. It now asks the generator which exists, so it
    spans both.
  * my first version of that detection used `grep -q` under `set -euo pipefail`:
    grep exits on first match, closing the pipe, cmake dies on SIGPIPE, and
    pipefail reports the pipeline as failed — a SUCCESSFUL match tested as a
    failure. Matched in pure bash now. It passes in a toy test because printf
    finishes before the pipe closes, so the race only appears with a slow
    producer.
  * Dockerfile.update-llama EXECUTED `rpc-server --help` at build time, which
    needs libcuda.so.1 — the driver, absent from any build by definition. The
    file's own comment eleven lines above states the rule ("the check is ldd,
    not import"); the wheel check obeyed it and this one did not. It had never
    been reachable before, so the first build to get that far could not pass.
    Now checks linkage and tolerates only libcuda being unresolved.

And Dockerfile.update now carries vosk and g++, because the incremental overlay
never touches the venv: an updated image shipped without vosk while /v1/models
advertised vosk-en and vosk-it, and without any C++ compiler at all, which is
what XTTS's Japanese phonemiser needs to build.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent 939a1f1c
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.5.52" __version__ = "0.5.53"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -172,37 +172,51 @@ is correct at two different model sizes, and Qwen3.5's OWN vision tower is ...@@ -172,37 +172,51 @@ is correct at two different model sizes, and Qwen3.5's OWN vision tower is
correct when transformers runs it instead of llama.cpp — so it is not the model, correct when transformers runs it instead of llama.cpp — so it is not the model,
not the family, and not our pipeline. not the family, and not our pipeline.
## The actual cause: our llama.cpp predates the fix ## A fourth wrong theory: the llama.cpp version
``` I claimed the cause was found, and it was not. Recorded in full because the
our build (0.3.30) vendors llama.cpp e3a74b299 2026-06-16 reasoning looked strong and was still worthless.
the fix b4aa7dd47 "mtmd : use align_corners for qwen3vl vision
position embedding interpolation" (#25781) 2026-07-21 The argument was: our 0.3.30 vendors llama.cpp e3a74b299 (2026-06-16); the
0.3.36 vendors llama.cpp 0c1e57098 2026-10-01 <- contains it commit b4aa7dd47 "mtmd : use align_corners for qwen3vl vision position embedding
``` interpolation" (#25781) landed 2026-07-21; 0.3.36 vendors 0c1e57098 (2026-10-01)
and so contains it. Wrong position-embedding interpolation would make vision
Five weeks. Wrong position-embedding interpolation makes the vision features features spatially meaningless, which fits a constant answer exactly, and
spatially meaningless, which is why the output is constant rather than merely `gemma4v` runs a different graph so it would be untouched. Every observation
wrong, and why `gemma4v` -- a different graph, untouched by that bug -- reads the fitted.
same images correctly through the same pipeline.
It was still only a plausible commit plus date arithmetic, and I wrote it into
Everything observed fits: the projector pairs correctly, loads, and injects these docs as "the actual cause" without having run it.
tokens the model notices (it says so when there is no image); two publishers'
conversions fail identically because the defect is in the consumer, not the **Tested: llama-cpp-python 0.3.36 was built from source with the full
artifact; and the image-token floor is irrelevant, confirmed by testing with it -DGGML_CUDA=ON -DGGML_VULKAN=ON -DGGML_RPC=ON set and put into service. The same
at 1024 and at 0 and getting byte-identical answers. two images still return byte-identical answers.** So align_corners was not it,
and the fourth theory joins the other three.
`packaging/linux/Dockerfile.update-llama` is bumped to 0.3.36 for this reason.
It takes effect only on a rebuild, so until one runs, `mmproj` stays off the What that does buy: the last standing candidate is eliminated BY TEST. The
qwen38 cards and vision goes to gemma-4-31B-heretic. remaining explanation is that llama.cpp's qwen3vl graph does not fit this model
at all -- the base is `qwen3_5` / `Qwen3_5ForConditionalGeneration` while the
One method note, since it cost most of the investigation: three conclusions were projector declares `qwen3vl_merger`, so a faithful implementation of the wrong
drawn from reading headers and code, and all three were wrong. The floor was graph on correctly-shaped tensors is still the best reading, and one that no
dismissed with "the symptom predates it" -- never checked, and it could not have version bump fixes.
been, because the floor is applied automatically whenever the projector type
matches. Two projectors were compared while the one variable introduced by this `mmproj` stays off the qwen38 cards; vision goes to gemma-4-31B-heretic.
repo was held constant in both. What settled it was varying one thing at a time
against a known-good control. ## The method, since it cost more than the finding
Four theories, each argued from reading rather than running: bad pairing, missing
deepstack, a bad artifact, an outdated llama.cpp. All four wrong.
The floor was dismissed with "the symptom predates it" -- never checked, and it
could not have been, because the floor is applied automatically whenever the
projector type matches, so every observation had it on. Two publishers'
projectors were then compared while the one variable this repo introduced was
held constant in both.
What actually produced knowledge: gemma as a known-good control through the same
pipeline, varying one thing at a time, and accepting a negative result. Three of
the four theories could have been killed in the time it took to write the
paragraph asserting them.
## If you try a newer llama.cpp, do NOT pip install it ## If you try a newer llama.cpp, do NOT pip install it
......
...@@ -57,9 +57,41 @@ CM=(-DGGML_RPC=ON -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_ ...@@ -57,9 +57,41 @@ CM=(-DGGML_RPC=ON -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_
[ -n "$CUDA" ] && CM+=(-DGGML_CUDA=ON ${CMAKE_CUDA_ARCHITECTURES:+-DCMAKE_CUDA_ARCHITECTURES="$CMAKE_CUDA_ARCHITECTURES"}) [ -n "$CUDA" ] && CM+=(-DGGML_CUDA=ON ${CMAKE_CUDA_ARCHITECTURES:+-DCMAKE_CUDA_ARCHITECTURES="$CMAKE_CUDA_ARCHITECTURES"})
[ -n "$VULKAN" ] && CM+=(-DGGML_VULKAN=ON) [ -n "$VULKAN" ] && CM+=(-DGGML_VULKAN=ON)
cmake -S "$SRC" -B build "${CM[@]}" ${EXTRA_CMAKE_ARGS:-} >/dev/null cmake -S "$SRC" -B build "${CM[@]}" ${EXTRA_CMAKE_ARGS:-} >/dev/null
cmake --build build --target rpc-server -j"$(nproc)"
BIN=$(find build -type f -name rpc-server | head -1) # The CMake target was renamed: tools/rpc/CMakeLists.txt declares
[ -x "$BIN" ] || { echo "rpc-server did not build" >&2; exit 1; } # `ggml-rpc-server` in current llama.cpp and `rpc-server` in older ones. Asking
# for the wrong one fails the whole image build with
# gmake: *** No rule to make target 'rpc-server'. Stop.
# which names the target but not the rename -- measured on the 0.3.30 -> 0.3.36
# bump, where the wheel itself built fine and only this step died. Ask the
# generator which targets exist rather than guessing, so the script spans both.
# Matched in pure bash, with the target list captured first. NOT
# `cmake --target help | grep -q`: this script runs under `set -euo pipefail`,
# and `grep -q` exits the moment it matches, which closes the pipe, kills cmake
# with SIGPIPE, and pipefail then reports the whole pipeline as failed -- so a
# SUCCESSFUL match tested as a failure and both candidates were skipped. The
# diagnostic below used `grep -i rpc` without -q, read to EOF, and printed the
# target it had just "not found", which is what gave it away.
TARGETS="$(cmake --build build --target help 2>/dev/null || true)"
RPC_TARGET=""
for cand in ggml-rpc-server rpc-server; do
# make lists targets as "... name"; some generators omit the prefix.
if [[ $'\n'"$TARGETS"$'\n' == *$'\n'"... $cand"$'\n'* ]] \
|| [[ $'\n'"$TARGETS"$'\n' == *$'\n'"$cand"$'\n'* ]]; then
RPC_TARGET="$cand"; break
fi
done
if [ -z "$RPC_TARGET" ]; then
echo "neither ggml-rpc-server nor rpc-server is a target in this llama.cpp;" >&2
echo "available targets containing 'rpc':" >&2
cmake --build build --target help 2>/dev/null | grep -i rpc >&2 || true
exit 1
fi
echo "rpc target: $RPC_TARGET"
cmake --build build --target "$RPC_TARGET" -j"$(nproc)"
BIN=$(find build -type f \( -name ggml-rpc-server -o -name rpc-server \) | head -1)
[ -x "$BIN" ] || { echo "$RPC_TARGET did not build" >&2; exit 1; }
# Installed under the name coderai invokes regardless of the upstream target.
install -m 755 "$BIN" "$DEST/rpc-server" install -m 755 "$BIN" "$DEST/rpc-server"
echo "installed $DEST/rpc-server" echo "installed $DEST/rpc-server (from $RPC_TARGET)"
"$DEST/rpc-server" --help 2>&1 | head -3 || true "$DEST/rpc-server" --help 2>&1 | head -3 || true
...@@ -54,6 +54,24 @@ RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir "nv ...@@ -54,6 +54,24 @@ RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir "nv
# without it (discovery just stays off), hence `|| true`. # without it (discovery just stays off), hence `|| true`.
RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir "zeroconf>=0.130" || true RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir "zeroconf>=0.130" || true
# Vosk, the CPU STT backend. It is declared in requirements.txt, which the FULL
# build picks up through the pre-built venv bundle -- but this overlay never
# touches the venv, so an image updated incrementally shipped without it and
# every vosk transcription answered
# Transcription error: No module named 'vosk'
# while /v1/models advertised vosk-en and vosk-it as available. Installed here
# for the same reason nvidia-ml-py and zeroconf are: the overlay has to carry
# what the bundle would have.
#
# Install into the venv's own site-packages explicitly. Installing it the
# ordinary way inside the running container put it in ~/.local instead, because
# the venv is not writable by the container user, and the engine -- which does
# not load the user site -- went on reporting the module as missing while pip
# reported success.
RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir \
--target /opt/coderai/python/lib/python3.13/site-packages \
"vosk>=0.3.45" || true
# openssh-client so `coderai-docker --upgrade --ssh-key …` can fetch over SSH # openssh-client so `coderai-docker --upgrade --ssh-key …` can fetch over SSH
# (git is present but the CUDA runtime base ships no ssh). Tiny; keep the overlay # (git is present but the CUDA runtime base ships no ssh). Tiny; keep the overlay
# still fast. `|| true` keeps offline overlay builds working (HTTPS upgrade still # still fast. `|| true` keeps offline overlay builds working (HTTPS upgrade still
...@@ -62,7 +80,15 @@ RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir "ze ...@@ -62,7 +80,15 @@ RUN /opt/coderai/python/bin/python3 -m pip install --no-input --no-cache-dir "ze
# model with real audio instead of a reachability check. Without it the probe # model with real audio instead of a reachability check. Without it the probe
# degrades silently to "configured, not verified" — caught by inspecting a built # degrades silently to "configured, not verified" — caught by inspecting a built
# image, not by any test, because the fallback is deliberately not an error. # image, not by any test, because the fallback is deliberately not an error.
RUN apt-get update && apt-get install -y --no-install-recommends openssh-client espeak \ # g++: a RUNTIME requirement, like the gcc the base carries for Triton. XTTS's
# Japanese phonemiser (cutlet) pulls mojimoji, a C++ extension with no cp313
# wheel, so it compiles when the isolated TTS venv is built on demand. The
# bundled Python records clang++ as its C++ compiler and no clang is present, so
# the build asked for clang++ and found nothing -- and the image had no C++
# compiler at all: g++, c++ and clang were all absent and /usr/include/c++ did
# not exist, which made gcc's presence look like it covered this. Without it
# XTTS loses exactly one of its seventeen languages.
RUN apt-get update && apt-get install -y --no-install-recommends openssh-client espeak g++ \
&& rm -rf /var/lib/apt/lists/* || true && rm -rf /var/lib/apt/lists/* || true
# Fonts for burned-in captions and overlays (codai/compose): Montserrat and # Fonts for burned-in captions and overlays (codai/compose): Montserrat and
......
...@@ -88,6 +88,27 @@ assert "libggml-rpc" in names or "ggml_backend_rpc" in syms, \ ...@@ -88,6 +88,27 @@ assert "libggml-rpc" in names or "ggml_backend_rpc" in syms, \
"RPC backend missing from the rebuilt llama-cpp-python" "RPC backend missing from the rebuilt llama-cpp-python"
print("llama-cpp-python links cleanly and carries the RPC backend: OK") print("llama-cpp-python links cleanly and carries the RPC backend: OK")
PY PY
# Run it the way the launcher does (local-libs on the library path); fail the # Check what it LINKS, not that it runs — the same rule the wheel check above
# build if it cannot start, not just print an error and carry on. # states and follows. Executing it here fails on a correctly built binary:
RUN LD_LIBRARY_PATH=/opt/coderai/python/lib:/opt/coderai/local-libs /opt/coderai/python/bin/rpc-server --help >/dev/null #
# rpc-server: error while loading shared libraries: libcuda.so.1
#
# libcuda.so.1 is the NVIDIA driver, mounted by the container runtime and so
# absent at build time by definition. The old `--help` invocation only became
# reachable once the build stopped failing earlier (the rpc-server CMake target
# was renamed to ggml-rpc-server upstream), and a CUDA-enabled rpc-server links
# the driver directly — so the first build that got this far could not pass.
#
# Every other dependency must still resolve: a missing libggml or libgomp here
# means a binary that will not start on a real host either.
RUN set -eux; \
LD_LIBRARY_PATH=/opt/coderai/python/lib:/opt/coderai/local-libs \
ldd /opt/coderai/python/bin/rpc-server > /tmp/rpc-ldd.txt || true; \
cat /tmp/rpc-ldd.txt; \
if grep -v 'libcuda\.so' /tmp/rpc-ldd.txt | grep -q 'not found'; then \
echo "rpc-server has unresolved libraries other than the driver:" >&2; \
grep 'not found' /tmp/rpc-ldd.txt >&2; \
exit 1; \
fi; \
rm -f /tmp/rpc-ldd.txt; \
echo "rpc-server links cleanly (libcuda.so.1 resolves at run time): OK"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment