Skip to content

fix(ds4): build CUDA kernels for the target architecture - #11840

Open
FiloSpaTeam wants to merge 2 commits into
mudler:masterfrom
FiloSpaTeam:fix/ds4-cuda-arch
Open

fix(ds4): build CUDA kernels for the target architecture#11840
FiloSpaTeam wants to merge 2 commits into
mudler:masterfrom
FiloSpaTeam:fix/ds4-cuda-arch

Conversation

@FiloSpaTeam

Copy link
Copy Markdown
Contributor

Description

The ds4 backend compiled its CUDA objects with no -arch. backend/cpp/ds4/Makefile
drives upstream's object targets directly ($(MAKE) -C ds4 ds4.o ds4_cuda.o ...),
which bypasses upstream's own guard — its cuda target refuses to build unless
CUDA_ARCH is set, offering cuda-spark (sm_121) and cuda-generic (native)
instead. nvcc therefore fell back to its default architecture and the kernels ran
as JIT'd PTX on the real device.

On a DGX Spark / GB10 (sm_121) that silently corrupted inference: any prompt over
roughly 128 tokens produced text unrelated to the input and never closed its
thinking block, so content came back empty and the chat showed only reasoning.
Longer prompts failed with ds4 generation failed: cuda decode failed. In a chat
this presents as "only the first message works", because the first prompt is short
and every later turn carries enough history to cross the boundary. Short prompts
stayed correct throughout, which is why nothing caught it.

Isolated against upstream ds4 alone, same box, same model, same prompt, same GPU,
differing only in the nvcc flags:

build output prefill
make -B ds4 (archless, as LocalAI built it) garbage 4.21 t/s
make cuda-spark (compute_121a/sm_121a) correct 325.70 t/s

Dockerfile.ds4 now forwards CUDA_MAJOR_VERSION, which the backend matrix
already declares for both ds4 cublas entries but nothing consumed, and the backend
Makefile selects a gencode list from it plus uname -m and passes it to the
sub-make as NVCC_ARCH_FLAGS. Upstream's CUDA_ARCH takes a single value so it
cannot express the fat binary these images need; a command-line assignment wins
over its :=. The architecture lists are copied from backend/go/vllm-cpp/Makefile
rather than invented, so the two CUDA images cover the same GPUs. An empty
CUDA_MAJOR_VERSION keeps upstream's native behaviour for local developer
builds; an unrecognised one is a hard error, since no CI runner has a GPU and a
silent native there is the exact failure being fixed.

DS4_CUDA_HAVE_MXF4 is deliberately left unset. Upstream defines it only for
single-arch sm_120/sm_121 builds and guards it with a plain #ifdef rather than
__CUDA_ARCH__, so it cannot be combined with older archs in one fat binary. It
gates an optional MXFP4 indexer fast path whose #ifndef branch returns 0 and
falls back to the generic path, so omitting it costs some speed on GB10, not
correctness. Worth revisiting if upstream adds __CUDA_ARCH__ guards.

The second commit adds the regression coverage that was missing: an opt-in
long_prefill capability in the backend e2e suite. Every existing spec uses a
short prompt, so the entire suite passed against a backend that produced garbage
in normal use.

Notes for Reviewers

How it was tested — all of the following on a DGX Spark (GB10, sm_121, CUDA 13.0,
DeepSeek-V4-Flash IQ2XXS), against ds4 pin 8db89fe:

  1. Root cause isolated with upstream ds4 alone, no LocalAI involved — the table above.
  2. Flag selection, all four paths:
    make -C backend/cpp/ds4 BUILD_TYPE=cublas CUDA_MAJOR_VERSION=13 NATIVE=false \
      --eval='show: ; @echo [$(DS4_ARCH_MAKEVARS)]' show
    
    amd64 list on x86_64; arm64 list on aarch64; empty CUDA_MAJOR_VERSION
    CUDA_ARCH=native; CUDA_MAJOR_VERSION=12 → hard error. (Do not use make -n
    here — the recipe is +$(MAKE) ... and the + prefix runs it even under -n.)
  3. Full native build on the GB10. All five arm64 architectures compiled, including
    sm_87 and sm_110; the gencode list appears on every nvcc line.
  4. Backend swapped into a running LocalAI and verified over the API with no
    prefill_chunk workaround in the model config. Prompts of 120/126/128/130/322/
    1022/2022 tokens all answer correctly with non-empty content and finish=stop;
    the multi-turn chat that originally failed returns a coherent answer.
  5. Time-to-first-token for a ~2010-token prompt dropped to 2.34s, from 23.85s
    when the corruption was worked around by capping prefill_chunk, and from 5.1s
    for the archless path that returned garbage.

Reproducing it — with a GPU whose architecture is outside nvcc's default, build
the backend, point a model at it and run:

BACKEND_BINARY=$(pwd)/backend/cpp/ds4/package/run.sh \
BACKEND_TEST_MODEL_FILE=/path/to/ds4flash.gguf \
BACKEND_TEST_CAPS=health,load,predict,long_prefill \
go test -count=1 -timeout=30m -v ./tests/e2e-backends/...

long_prefill fails on an archless build and passes on an arch-correct one, while
predict passes either way. .agents/ds4-backend.md documents both this and the
flag check.

Not verified — the amd64 architecture list is exercised by neither the local
build nor the checks above, only by CI. 103a is the entry I'd watch. Eight
architectures on a 1.4 MB .cu will also lengthen that build noticeably; trimming
the list is reasonable if the cost is unwelcome, since ds4 needs ~80 GB resident on
CUDA and the smaller consumer parts cannot run it regardless.

Possibly relatedgo/moss-transcribe-cpp looks like the same class of bug and
ships four CUDA images: upstream's CMakeLists.txt:19 does
set(GGML_NATIVE ON CACHE BOOL "" FORCE), which overrides the -DGGML_NATIVE=OFF
the backend passes, so vendored ggml selects CMAKE_CUDA_ARCHITECTURES=native on a
GPU-less runner. Not addressed here; happy to open it separately. The llama-cpp
family is unaffected — ggml's own default list is sane.

Signed commits

  • Yes, I signed my commits.
  • Documentation updated (docs/content/) for user-facing changes, or not applicable

The ds4 backend compiled its CUDA objects with no -arch. Upstream's Makefile
leaves CUDA_ARCH empty and its `cuda` target refuses to build without one,
offering `cuda-spark` (sm_121) and `cuda-generic` (native) instead. We invoke
its object targets directly, which bypasses that guard, so nvcc fell back to
its default architecture and the kernels ran as JIT'd PTX on the real GPU.

On GB10 (sm_121) that silently corrupted inference: any prompt over roughly 128
tokens produced text unrelated to the input and never closed its thinking
block, so content came back empty and the chat showed only reasoning; longer
prompts failed with "cuda decode failed". It also cost close to two orders of
magnitude of prefill throughput. Measured on one box, same model, same prompt,
same GPU, upstream ds4 at the pinned commit, differing only in the nvcc flags:

  make -B ds4      (archless, as we build it)   garbage output    4.21 t/s
  make cuda-spark  (compute_121a/sm_121a)       correct output  325.70 t/s

Select an architecture list from CUDA_MAJOR_VERSION, which the backend matrix
already declares for both ds4 cublas entries but Dockerfile.ds4 never forwarded.
Upstream's CUDA_ARCH takes a single value, so it cannot express the fat binary
these images need; NVCC_ARCH_FLAGS is overridden instead, since a command-line
assignment wins over its `:=`. The lists are copied from vllm-cpp rather than
invented so the two CUDA images cover the same GPUs, with l4t/arm64 covering
Orin, Thor and GB10. An empty CUDA_MAJOR_VERSION keeps upstream's `native`
behaviour for local developer builds, and no CI runner has a GPU to enumerate.

DS4_CUDA_HAVE_MXF4 is deliberately left unset: upstream defines it only for
single-arch sm_120/sm_121 builds and guards it with a plain #ifdef rather than
__CUDA_ARCH__, so it cannot be combined with older archs. It gates an optional
MXFP4 indexer fast path whose #ifndef branch returns 0 and falls back cleanly,
so omitting it costs speed on GB10, not correctness.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>
The architecture fix has no automated guard: every existing e2e spec uses a
short prompt, and the miscompiled backend answered short prompts correctly.
The corruption only appears once a prompt spans more than one prefill batch,
so the whole suite passed against a backend that produced garbage in normal
use.

Add an opt-in "long_prefill" capability to the backend e2e suite that sends a
prompt well past one batch with a known needle and asserts the answer still
reflects it, and document in the ds4 guide why the build must never omit an
nvcc architecture, how to check which flags a configuration resolves to
without compiling, and how to run the new spec.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Claudio Maradonna <git@codeshifter.xyz>
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.

1 participant