From 2994244960e6e6c7f194c531c931024bb555de1b Mon Sep 17 00:00:00 2001 From: worthant Date: Tue, 21 Jul 2026 18:19:12 +0300 Subject: [PATCH 1/7] tests: fix test-llama-archs failure on laguna - moe_mandatory: laguna is sigmoid-routed MoE with a mandatory shared expert; testing it as dense fails on required expert hparams - fixture: write expert_shared_feed_forward_length, expert_weights_scale and expert_weights_norm for MoE configs (laguna requires all three) - laguna: when tensor metadata is unavailable (metadata-only loads used by test-llama-archs) default the attention gate width to per-head; real files with a missing gate still fail in required create_tensor - model-saver: n_ff_chexp was written under the shared-expert FF key, overwriting n_ff_shexp with 0 on save/reload roundtrip; write it under EXPERT_CHUNK_FEED_FORWARD_LENGTH where it belongs --- src/llama-model-saver.cpp | 2 +- src/models/laguna.cpp | 7 +++++-- tests/test-llama-archs.cpp | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp index 0d39901abf71..5f448cd837fd 100644 --- a/src/llama-model-saver.cpp +++ b/src/llama-model-saver.cpp @@ -213,7 +213,7 @@ void llama_model_saver::add_kv_from_model() { add_kv(LLM_KV_FEED_FORWARD_LENGTH, hparams.n_ff_arr, true); add_kv(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp); add_kv(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp); - add_kv(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_chexp); + add_kv(LLM_KV_EXPERT_CHUNK_FEED_FORWARD_LENGTH, hparams.n_ff_chexp); add_kv(LLM_KV_SWIGLU_CLAMP_EXP, hparams.swiglu_clamp_exp); add_kv(LLM_KV_SWIGLU_CLAMP_SHEXP, hparams.swiglu_clamp_shexp); add_kv(LLM_KV_USE_PARALLEL_RESIDUAL, hparams.use_par_res); diff --git a/src/models/laguna.cpp b/src/models/laguna.cpp index e274739395ea..d4e913bb2724 100644 --- a/src/models/laguna.cpp +++ b/src/models/laguna.cpp @@ -101,9 +101,12 @@ void llama_model_laguna::load_arch_tensors(llama_model_loader & ml) { // `gating` type and fails at conversion time on a mismatch.) const int64_t n_gate_per_head = n_head_il; const int64_t n_gate_per_elem = n_embd_head_k * n_head_il; + // Metadata-only loads (test-llama-archs synthesizes models without a + // tensor map) have no meta to inspect -- default to per-head there. A + // real file with the gate tensor missing still fails hard in the + // required create_tensor below. const ggml_tensor * gate_meta = ml.get_tensor_meta(tn(LLM_TENSOR_ATTN_GATE, "weight", i).str().c_str()); - GGML_ASSERT(gate_meta != nullptr && "Laguna: missing attention gate tensor"); - const int64_t n_gate_out = gate_meta->ne[1]; + const int64_t n_gate_out = gate_meta != nullptr ? gate_meta->ne[1] : n_gate_per_head; if (n_gate_out != n_gate_per_head && n_gate_out != n_gate_per_elem) { GGML_ABORT("Laguna: unexpected attention gate width %lld at layer %d " "(expected %lld per-head or %lld per-element)", diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index ec7e17f7e4b2..7112970f3482 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -213,6 +213,9 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_EXPERT_GATING_FUNC, uint32_t(2)); // sigmoid ms.add_kv(LLM_KV_EXPERT_GROUP_SCALE, 1.0f); ms.add_kv(LLM_KV_EXPERTS_PER_GROUP, uint32_t(1)); + ms.add_kv(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, n_ff); + ms.add_kv(LLM_KV_EXPERT_WEIGHTS_SCALE, 1.0f); + ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, false); } ms.add_kv(LLM_KV_POSNET_EMBEDDING_LENGTH, n_embd); @@ -343,6 +346,7 @@ static bool moe_mandatory(const llm_arch arch) { case LLM_ARCH_BAILINGMOE2: case LLM_ARCH_DOTS1: case LLM_ARCH_AFMOE: + case LLM_ARCH_LAGUNA: case LLM_ARCH_ERNIE4_5: case LLM_ARCH_ERNIE4_5_MOE: case LLM_ARCH_HUNYUAN_MOE: From 5ace274bf9749508f7d4a5b217984a89fc8ad2ee Mon Sep 17 00:00:00 2001 From: worthant Date: Tue, 21 Jul 2026 18:56:15 +0300 Subject: [PATCH 2/7] ci: always report required checks on pull requests build-vulkan.yml only fired on PRs touching ggml-vulkan sources and dev-build.yml only on C/C++ changes, but their jobs are required status checks on dev -- any PR outside those paths (like this one) hangs on 'Expected -- waiting for status to be reported' with no way to satisfy it. Drop the paths filters from the pull_request triggers (push triggers keep theirs) and let dev-build also run on PRs into master so the dev -> master promotion PR reports the same checks. --- .github/workflows/build-vulkan.yml | 8 ++++---- .github/workflows/dev-build.yml | 18 +++++------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-vulkan.yml b/.github/workflows/build-vulkan.yml index a103c50faf70..a20541d94c1a 100644 --- a/.github/workflows/build-vulkan.yml +++ b/.github/workflows/build-vulkan.yml @@ -17,12 +17,12 @@ on: '**/*.glsl' ] + # No paths filter: ubuntu-llvmpipe/ubuntu-arm64 are required status checks + # on dev, and a required check that never reports leaves the PR stuck in + # "Expected" forever (e.g. a docs-only PR). This is also the only PR job + # that runs the full ctest suite, so running it on every PR is intended. pull_request: types: [opened, synchronize, reopened] - paths: [ - '.github/workflows/build-vulkan.yml', - 'ggml/src/ggml-vulkan/**' - ] concurrency: group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 9d86c25f270a..ef0ebba7cfb5 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -27,22 +27,14 @@ on: - '**/*.comp' - '**/*.glsl' - '**/*.metal' + # No paths filter: these jobs are required status checks on dev, and a + # required check that never reports leaves the PR stuck in "Expected" + # forever (e.g. a docs-only PR). Runs on PRs into master too so the + # dev -> master promotion PR reports the same checks. pull_request: branches: - dev - paths: - - '.github/workflows/dev-build.yml' - - '.github/actions/windows-setup-cuda/**' - - '**/CMakeLists.txt' - - '**/*.h' - - '**/*.hpp' - - '**/*.c' - - '**/*.cpp' - - '**/*.cu' - - '**/*.cuh' - - '**/*.comp' - - '**/*.glsl' - - '**/*.metal' + - master # Group by ref so consecutive pushes to dev serialize: a newer push cancels # the older in-flight build instead of racing it for the dev-latest release. From 1630a0f9355e134b391aa216b9e8eb2a596e5496 Mon Sep 17 00:00:00 2001 From: worthant Date: Tue, 21 Jul 2026 20:14:35 +0300 Subject: [PATCH 3/7] quantize: expose NVFP4 as a quantization target; ship llama-quantize in archives ggml already implements quantize_nvfp4 (incl. imatrix) and ggml_quantize_chunk dispatches it, but neither llama-quant.cpp nor the quantize tool mapped LLAMA_FTYPE_MOSTLY_NVFP4 to it, so NVFP4 GGUFs could only be obtained by converting pre-quantized checkpoints. Wire the ftype through and list NVFP4 in llama-quantize. Also package llama-quantize into the linux/macos dev and release archives (windows already ships it via Release\*) so a downloaded dev build can produce test quants without a local checkout. Verified locally: SmolLM2-135M f16 -> NVFP4 (259MB -> 88MB), coherent output via llama-cli and llama-server (dev-latest binary) on CPU. --- .github/workflows/dev-build.yml | 4 +++- .github/workflows/release-turboquant.yml | 8 +++++--- src/llama-quant.cpp | 1 + tools/quantize/quantize.cpp | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index ef0ebba7cfb5..2c5e40a6ebfa 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -103,6 +103,7 @@ jobs: cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true + cp build/bin/llama-quantize release/build/bin/ 2>/dev/null || true find build/bin -name "*.so*" -exec cp -P {} release/build/bin/ \; 2>/dev/null || true cp LICENSE release/build/bin/ 2>/dev/null || true cd release @@ -292,7 +293,7 @@ jobs: - name: Sign binaries if: ${{ github.event_name != 'pull_request' }} run: | - for bin in build/bin/llama-server build/bin/llama-cli build/bin/llama-bench build/bin/llama-perplexity; do + for bin in build/bin/llama-server build/bin/llama-cli build/bin/llama-bench build/bin/llama-perplexity build/bin/llama-quantize; do if [ -f "$bin" ]; then codesign --force --options runtime --timestamp \ --entitlements .github/entitlements.plist \ @@ -312,6 +313,7 @@ jobs: cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true + cp build/bin/llama-quantize release/build/bin/ 2>/dev/null || true cd release zip -r ../llama-turboquant-macos-arm64.zip . tar -czf ../llama-turboquant-macos-arm64.tar.gz . diff --git a/.github/workflows/release-turboquant.yml b/.github/workflows/release-turboquant.yml index 34fa87258672..5b1765bacdd1 100644 --- a/.github/workflows/release-turboquant.yml +++ b/.github/workflows/release-turboquant.yml @@ -106,6 +106,7 @@ jobs: cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true + cp build/bin/llama-quantize release/build/bin/ 2>/dev/null || true find build/bin -name "*.so*" -exec cp -P {} release/build/bin/ \; 2>/dev/null || true cp LICENSE release/build/bin/ 2>/dev/null || true cd release @@ -290,7 +291,7 @@ jobs: - name: Sign binaries run: | - for bin in build/bin/llama-server build/bin/llama-cli build/bin/llama-bench build/bin/llama-perplexity; do + for bin in build/bin/llama-server build/bin/llama-cli build/bin/llama-bench build/bin/llama-perplexity build/bin/llama-quantize; do if [ -f "$bin" ]; then codesign --force --options runtime --timestamp \ --entitlements .github/entitlements.plist \ @@ -306,6 +307,7 @@ jobs: cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true + cp build/bin/llama-quantize release/build/bin/ 2>/dev/null || true cd release zip -r ../llama-turboquant-macos-arm64.zip . tar -czf ../llama-turboquant-macos-arm64.tar.gz . @@ -321,7 +323,7 @@ jobs: --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ --wait --timeout 10m - for bin in build/bin/llama-server build/bin/llama-cli build/bin/llama-bench build/bin/llama-perplexity; do + for bin in build/bin/llama-server build/bin/llama-cli build/bin/llama-bench build/bin/llama-perplexity build/bin/llama-quantize; do if [ -f "$bin" ]; then name=$(basename "$bin") zip -j "${name}.zip" "$bin" @@ -392,7 +394,7 @@ jobs: ### What's included - \`llama-server\` with \`--cache-type-k turbo3\` / \`turbo4\` support - - \`llama-cli\`, \`llama-bench\`, \`llama-perplexity\` + - \`llama-cli\`, \`llama-bench\`, \`llama-perplexity\`, \`llama-quantize\` - All supported backends in one release: | Backend | Asset | diff --git a/src/llama-quant.cpp b/src/llama-quant.cpp index 7b4b5e637a5a..e2bf46fe90ff 100644 --- a/src/llama-quant.cpp +++ b/src/llama-quant.cpp @@ -814,6 +814,7 @@ ggml_type llama_ftype_get_default_type(llama_ftype ftype) { case LLAMA_FTYPE_MOSTLY_Q2_0: return GGML_TYPE_Q2_0; case LLAMA_FTYPE_MOSTLY_MXFP4_MOE: return GGML_TYPE_MXFP4; + case LLAMA_FTYPE_MOSTLY_NVFP4: return GGML_TYPE_NVFP4; // K-quants case LLAMA_FTYPE_MOSTLY_Q2_K_S: diff --git a/tools/quantize/quantize.cpp b/tools/quantize/quantize.cpp index dfb5ebdfae70..fe127bf3e730 100644 --- a/tools/quantize/quantize.cpp +++ b/tools/quantize/quantize.cpp @@ -37,6 +37,7 @@ static const std::vector QUANT_OPTIONS = { { "Q4_0", LLAMA_FTYPE_MOSTLY_Q4_0, " 4.34G, +0.4685 ppl @ Llama-3-8B", }, { "Q4_1", LLAMA_FTYPE_MOSTLY_Q4_1, " 4.78G, +0.4511 ppl @ Llama-3-8B", }, { "MXFP4_MOE",LLAMA_FTYPE_MOSTLY_MXFP4_MOE," MXFP4 MoE", }, + { "NVFP4", LLAMA_FTYPE_MOSTLY_NVFP4, " 4.50 bpw NVIDIA FP4", }, { "Q5_0", LLAMA_FTYPE_MOSTLY_Q5_0, " 5.21G, +0.1316 ppl @ Llama-3-8B", }, { "Q5_1", LLAMA_FTYPE_MOSTLY_Q5_1, " 5.65G, +0.1062 ppl @ Llama-3-8B", }, { "IQ2_XXS", LLAMA_FTYPE_MOSTLY_IQ2_XXS, " 2.06 bpw quantization", }, From b8f9a706f553bba5170bae8417f94b463f430cce Mon Sep 17 00:00:00 2001 From: worthant Date: Wed, 22 Jul 2026 02:11:44 +0300 Subject: [PATCH 4/7] ci: add linux-x64-cuda-13.3 build to dev and release pipelines Linux until now shipped only the Vulkan backend; NVIDIA users got the dequant path and no access to native CUDA kernels (incl. FP4 on Blackwell). Add a CUDA 13.3 build mirroring the Windows cuda-13.3 variant: GGML_BACKEND_DL keeps it a dlopen'd libggml-cuda.so next to the CPU variants, cudart/cublas are bundled like the Windows DLLs, archs cover A100/RTX30/RTX40/H100/RTX50 (80;86;89;90;120). --- .github/workflows/dev-build.yml | 83 ++++++++++++++++++++++- .github/workflows/release-turboquant.yml | 84 +++++++++++++++++++++++- 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 2c5e40a6ebfa..db4a7b143c86 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -119,6 +119,87 @@ jobs: llama-turboquant-linux-x64-vulkan.tar.gz retention-days: 14 + linux-x64-cuda-13.3: + runs-on: ubuntu-22.04 + + steps: + - name: Clone + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: ccache + uses: ggml-org/ccache-action@v1.2.21 + with: + key: turboquant-linux-x64-cuda-13.3 + evict-old-files: 1d + + - name: Install CUDA Toolkit + run: | + wget -q https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb + sudo dpkg -i cuda-keyring_1.1-1_all.deb + sudo apt-get update -y + sudo apt-get install -y build-essential \ + cuda-nvcc-13-3 cuda-cccl-13-3 cuda-cudart-dev-13-3 libcublas-dev-13-3 + echo "/usr/local/cuda/bin" >> "$GITHUB_PATH" + + - name: Build + # Arch set: A100 (80), RTX 30 (86), RTX 40 (89), H100/H200 (90), + # RTX 50 / Blackwell (120). Runner has no GPU -- build only, the + # backend is a dlopen'd libggml-cuda.so thanks to GGML_BACKEND_DL. + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_RPATH='$ORIGIN' \ + -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ + -DGGML_BACKEND_DL=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_CPU_ALL_VARIANTS=ON \ + -DGGML_CUDA=ON \ + -DGGML_CUDA_CUB_3DOT2=ON \ + -DCMAKE_CUDA_ARCHITECTURES="80;86;89;90;120" \ + -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache \ + -DLLAMA_CURL=OFF \ + -DLLAMA_OPENSSL=OFF \ + -DLLAMA_BUILD_SERVER=ON \ + -DLLAMA_BUILD_TOOLS=ON \ + -DLLAMA_BUILD_TESTS=OFF \ + -DLLAMA_BUILD_EXAMPLES=OFF + cmake --build build --config Release -j $(nproc) + + - name: Verify build + run: | + ./build/bin/llama-server --version 2>&1 || true + ls -l build/bin/libggml-cuda.so + + - name: Prepare archive + run: | + mkdir -p release/build/bin + cp build/bin/llama-server release/build/bin/ + cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true + cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true + cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true + cp build/bin/llama-quantize release/build/bin/ 2>/dev/null || true + find build/bin -name "*.so*" -exec cp -P {} release/build/bin/ \; 2>/dev/null || true + # CUDA runtime is not a given on user systems -- bundle it like the + # Windows job bundles cudart/cublas DLLs. + cp -P /usr/local/cuda/lib64/libcudart.so* release/build/bin/ + cp -P /usr/local/cuda/lib64/libcublas.so* release/build/bin/ + cp -P /usr/local/cuda/lib64/libcublasLt.so* release/build/bin/ + cp LICENSE release/build/bin/ 2>/dev/null || true + cd release + zip -r ../llama-turboquant-linux-x64-cuda-13.3.zip . + tar -czf ../llama-turboquant-linux-x64-cuda-13.3.tar.gz . + + - name: Upload archive + uses: actions/upload-artifact@v4 + with: + name: archive-linux-x64-cuda-13.3 + path: | + llama-turboquant-linux-x64-cuda-13.3.zip + llama-turboquant-linux-x64-cuda-13.3.tar.gz + retention-days: 14 + windows-x64: runs-on: windows-2022 @@ -337,7 +418,7 @@ jobs: # broken backend never blocks the others from shipping to testers — the # release notes call out what is missing. publish-dev-latest: - needs: [linux-x64-vulkan, windows-x64, macos-arm64] + needs: [linux-x64-vulkan, linux-x64-cuda-13.3, windows-x64, macos-arm64] if: ${{ always() && github.event_name == 'push' && github.ref == 'refs/heads/dev' }} runs-on: ubuntu-22.04 permissions: diff --git a/.github/workflows/release-turboquant.yml b/.github/workflows/release-turboquant.yml index 5b1765bacdd1..1f7f9d9af8ca 100644 --- a/.github/workflows/release-turboquant.yml +++ b/.github/workflows/release-turboquant.yml @@ -122,6 +122,88 @@ jobs: llama-turboquant-linux-x64-vulkan.tar.gz retention-days: 30 + linux-x64-cuda-13.3: + needs: verify-version + runs-on: ubuntu-22.04 + + steps: + - name: Clone + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: ccache + uses: ggml-org/ccache-action@v1.2.21 + with: + key: turboquant-linux-x64-cuda-13.3 + evict-old-files: 1d + + - name: Install CUDA Toolkit + run: | + wget -q https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb + sudo dpkg -i cuda-keyring_1.1-1_all.deb + sudo apt-get update -y + sudo apt-get install -y build-essential \ + cuda-nvcc-13-3 cuda-cccl-13-3 cuda-cudart-dev-13-3 libcublas-dev-13-3 + echo "/usr/local/cuda/bin" >> "$GITHUB_PATH" + + - name: Build + # Arch set: A100 (80), RTX 30 (86), RTX 40 (89), H100/H200 (90), + # RTX 50 / Blackwell (120). Runner has no GPU -- build only, the + # backend is a dlopen'd libggml-cuda.so thanks to GGML_BACKEND_DL. + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_RPATH='$ORIGIN' \ + -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ + -DGGML_BACKEND_DL=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_CPU_ALL_VARIANTS=ON \ + -DGGML_CUDA=ON \ + -DGGML_CUDA_CUB_3DOT2=ON \ + -DCMAKE_CUDA_ARCHITECTURES="80;86;89;90;120" \ + -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache \ + -DLLAMA_CURL=OFF \ + -DLLAMA_OPENSSL=OFF \ + -DLLAMA_BUILD_SERVER=ON \ + -DLLAMA_BUILD_TOOLS=ON \ + -DLLAMA_BUILD_TESTS=OFF \ + -DLLAMA_BUILD_EXAMPLES=OFF + cmake --build build --config Release -j $(nproc) + + - name: Verify build + run: | + ./build/bin/llama-server --version 2>&1 || true + ls -l build/bin/libggml-cuda.so + + - name: Prepare archive + run: | + mkdir -p release/build/bin + cp build/bin/llama-server release/build/bin/ + cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true + cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true + cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true + cp build/bin/llama-quantize release/build/bin/ 2>/dev/null || true + find build/bin -name "*.so*" -exec cp -P {} release/build/bin/ \; 2>/dev/null || true + # CUDA runtime is not a given on user systems -- bundle it like the + # Windows job bundles cudart/cublas DLLs. + cp -P /usr/local/cuda/lib64/libcudart.so* release/build/bin/ + cp -P /usr/local/cuda/lib64/libcublas.so* release/build/bin/ + cp -P /usr/local/cuda/lib64/libcublasLt.so* release/build/bin/ + cp LICENSE release/build/bin/ 2>/dev/null || true + cd release + zip -r ../llama-turboquant-linux-x64-cuda-13.3.zip . + tar -czf ../llama-turboquant-linux-x64-cuda-13.3.tar.gz . + + - name: Upload archive + uses: actions/upload-artifact@v4 + with: + name: archive-linux-x64-cuda-13.3 + path: | + llama-turboquant-linux-x64-cuda-13.3.zip + llama-turboquant-linux-x64-cuda-13.3.tar.gz + retention-days: 14 + windows-x64: needs: verify-version runs-on: windows-2022 @@ -353,7 +435,7 @@ jobs: # A stable release must be COMPLETE: this job has hard `needs` on every # build job — if anything failed, no release is published at all. publish-release: - needs: [verify-version, linux-x64-vulkan, windows-x64, macos-arm64] + needs: [verify-version, linux-x64-vulkan, linux-x64-cuda-13.3, windows-x64, macos-arm64] runs-on: ubuntu-22.04 permissions: contents: write From 9f4605dc26156a1eaa7c6660d5ebf06297dda2b6 Mon Sep 17 00:00:00 2001 From: worthant Date: Wed, 22 Jul 2026 02:23:08 +0300 Subject: [PATCH 5/7] ci: job IDs cannot contain dots -- rename to linux-x64-cuda-13-3, keep dotted display name --- .github/workflows/dev-build.yml | 5 +++-- .github/workflows/release-turboquant.yml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index db4a7b143c86..1bf8015606c1 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -119,7 +119,8 @@ jobs: llama-turboquant-linux-x64-vulkan.tar.gz retention-days: 14 - linux-x64-cuda-13.3: + linux-x64-cuda-13-3: + name: linux-x64-cuda-13.3 runs-on: ubuntu-22.04 steps: @@ -418,7 +419,7 @@ jobs: # broken backend never blocks the others from shipping to testers — the # release notes call out what is missing. publish-dev-latest: - needs: [linux-x64-vulkan, linux-x64-cuda-13.3, windows-x64, macos-arm64] + needs: [linux-x64-vulkan, linux-x64-cuda-13-3, windows-x64, macos-arm64] if: ${{ always() && github.event_name == 'push' && github.ref == 'refs/heads/dev' }} runs-on: ubuntu-22.04 permissions: diff --git a/.github/workflows/release-turboquant.yml b/.github/workflows/release-turboquant.yml index 1f7f9d9af8ca..d1d1728b257a 100644 --- a/.github/workflows/release-turboquant.yml +++ b/.github/workflows/release-turboquant.yml @@ -122,7 +122,8 @@ jobs: llama-turboquant-linux-x64-vulkan.tar.gz retention-days: 30 - linux-x64-cuda-13.3: + linux-x64-cuda-13-3: + name: linux-x64-cuda-13.3 needs: verify-version runs-on: ubuntu-22.04 @@ -435,7 +436,7 @@ jobs: # A stable release must be COMPLETE: this job has hard `needs` on every # build job — if anything failed, no release is published at all. publish-release: - needs: [verify-version, linux-x64-vulkan, linux-x64-cuda-13.3, windows-x64, macos-arm64] + needs: [verify-version, linux-x64-vulkan, linux-x64-cuda-13-3, windows-x64, macos-arm64] runs-on: ubuntu-22.04 permissions: contents: write From d0857e24996f12b06b63d249da4dade9bc7fdd4f Mon Sep 17 00:00:00 2001 From: worthant Date: Wed, 22 Jul 2026 03:47:03 +0300 Subject: [PATCH 6/7] ci: GPU smoke test of released backends on rented vast.ai boxes On-demand (workflow_dispatch) + nightly: rents the cheapest matching GPU, downloads the released archive, quantizes a tiny f16 model to NVFP4 with the shipped llama-quantize, serves it with -ngl 99, asserts a coherent answer, runs llama-bench and asserts the GPU backend actually did the work -- a silent CPU fallback fails the run. Result is posted as a non-required commit status gpu-smoke/ on the released commit. Deliberately NOT a required PR check: spot GPU rental is slow, nondeterministic and costs money. Hardening baked in from manual runs: X11 client libs + LunarG loader for the NVIDIA Vulkan ICD in headless containers, nohup+poll against vast hosts dropping long ssh sessions, no grep|head under pipefail, box is destroyed in an always() step. --- .github/scripts/gpu-smoke/remote-smoke.sh | 96 ++++++++++++++++ .github/scripts/gpu-smoke/rent.sh | 74 +++++++++++++ .github/workflows/backend-smoke.yml | 128 ++++++++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100755 .github/scripts/gpu-smoke/remote-smoke.sh create mode 100755 .github/scripts/gpu-smoke/rent.sh create mode 100644 .github/workflows/backend-smoke.yml diff --git a/.github/scripts/gpu-smoke/remote-smoke.sh b/.github/scripts/gpu-smoke/remote-smoke.sh new file mode 100755 index 000000000000..5050a70ac92a --- /dev/null +++ b/.github/scripts/gpu-smoke/remote-smoke.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Runs ON the rented GPU box. Smokes one released backend archive end-to-end: +# download release asset -> quantize f16 -> NVFP4 with the SHIPPED +# llama-quantize -> llama-server on GPU -> coherence assert -> llama-bench +# -> assert the GPU backend actually did the work (no silent CPU fallback). +# +# args: $1 = backend id (linux-x64-vulkan | linux-x64-cuda-13.3) +# $2 = release tag (e.g. dev-latest or b10018-1.0.0) +# Writes /root/smoke.log (progress) and /root/smoke.status (OK/FAIL last line). +set -uo pipefail # NOT -e: we handle failures explicitly to always write status + +BACKEND="${1:?backend id required}" +TAG="${2:-dev-latest}" +REPO="AtomicBot-ai/atomic-llama-cpp-turboquant" +WORK=/root/smoke +BIN="$WORK/bin/build/bin" + +fail() { echo "FAIL: $*"; echo "FAIL" > /root/smoke.status; exit 1; } + +mkdir -p "$WORK" && cd "$WORK" +export DEBIAN_FRONTEND=noninteractive + +echo "== [1/6] runtime deps for $BACKEND ==" +apt-get update -q >/dev/null 2>&1 || true +apt-get install -yq curl jq >/dev/null 2>&1 || fail "apt basic deps" +if [ "$BACKEND" = "linux-x64-vulkan" ]; then + # Lessons from manual runs on vast boxes: + # - libGLX_nvidia (the vulkan ICD) silently needs X11 client libs + # - the stock jammy vulkan loader (1.3.204) cannot negotiate with the ICD + # of current NVIDIA drivers -> take the loader from LunarG + apt-get install -yq libvulkan1 libxext6 libx11-6 wget gnupg >/dev/null 2>&1 || fail "apt vulkan deps" + wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | apt-key add - >/dev/null 2>&1 + wget -qO /etc/apt/sources.list.d/lunarg-vulkan-jammy.list \ + https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list + apt-get update -q >/dev/null 2>&1 + apt-get install -yq vulkan-sdk >/dev/null 2>&1 || fail "apt vulkan-sdk (LunarG)" +fi +# CUDA backend: driver comes from the host, cudart/cublas are bundled in the archive. + +echo "== [2/6] release asset ==" +curl -sfLo bin.tar.gz \ + "https://github.com/$REPO/releases/download/$TAG/llama-turboquant-$BACKEND.tar.gz" \ + || fail "asset download llama-turboquant-$BACKEND.tar.gz @ $TAG" +mkdir -p bin && tar xzf bin.tar.gz -C bin || fail "unpack" +VERSION_LINE=$("$BIN/llama-server" --version 2>&1 | head -1) +echo "version: $VERSION_LINE" +echo "$VERSION_LINE" | grep -q "version:" || fail "llama-server --version" + +echo "== [3/6] f16 -> NVFP4 with shipped llama-quantize ==" +curl -sfLo m-f16.gguf \ + "https://huggingface.co/bartowski/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct-f16.gguf" \ + || fail "model download" +"$BIN/llama-quantize" m-f16.gguf m-nvfp4.gguf NVFP4 >quant.log 2>&1 || fail "llama-quantize NVFP4" +[ -s m-nvfp4.gguf ] || fail "nvfp4 gguf empty" +ls -lh m-*.gguf + +echo "== [4/6] llama-server on GPU ==" +"$BIN/llama-server" -m m-nvfp4.gguf --port 8099 --no-webui -ngl 99 >server.log 2>&1 & +SRV=$! +UP="" +for i in $(seq 1 90); do + if curl -sf http://127.0.0.1:8099/health >/dev/null 2>&1; then UP=1; break; fi + kill -0 $SRV 2>/dev/null || break + sleep 2 +done +[ -n "$UP" ] || { tail -20 server.log; fail "server did not become healthy"; } +echo "health: ok" + +echo "== [5/6] generation + coherence assert ==" +ANSWER=$(curl -sf http://127.0.0.1:8099/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -d '{"messages":[{"role":"user","content":"What is the capital of France? Reply with just the city name."}],"max_tokens":20,"temperature":0}' \ + | jq -r '.choices[0].message.content // empty') +echo "answer: $ANSWER" +echo "$ANSWER" | grep -qi paris || { kill $SRV 2>/dev/null; fail "answer lacks 'Paris'"; } +kill $SRV 2>/dev/null; wait $SRV 2>/dev/null + +echo "== [6/6] llama-bench + GPU-actually-used assert ==" +"$BIN/llama-bench" -m m-nvfp4.gguf -ngl 99 -p 512 -n 128 >bench.log 2>&1 || fail "llama-bench" +sed -n '/| model/,$p' bench.log +case "$BACKEND" in + linux-x64-vulkan) + grep -q "load_backend: loaded Vulkan backend" bench.log || fail "Vulkan backend not loaded" + grep -Eq "ggml_vulkan: 0 = NVIDIA" bench.log || fail "Vulkan device is not the NVIDIA GPU" + ;; + linux-x64-cuda-13.3) + grep -q "load_backend: loaded CUDA backend" bench.log || fail "CUDA backend not loaded" + grep -Eiq "cuda.*(NVIDIA|GeForce|RTX|H[0-9]+)" bench.log || fail "CUDA device is not an NVIDIA GPU" + ;; + *) fail "unknown backend $BACKEND" ;; +esac +TG=$(awk -F'|' '/tg128/ {gsub(/ /,"",$7); print $7}' bench.log | head -1) +echo "tg128: ${TG:-?} t/s" + +echo "SMOKE OK: $BACKEND @ $TAG ($VERSION_LINE)" +echo "OK" > /root/smoke.status diff --git a/.github/scripts/gpu-smoke/rent.sh b/.github/scripts/gpu-smoke/rent.sh new file mode 100755 index 000000000000..bde32b4d7725 --- /dev/null +++ b/.github/scripts/gpu-smoke/rent.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Rent ONE vast.ai GPU box for the smoke test: walk the cheapest offers, +# create sequentially, first to reach `running` + answer ssh wins. +# Losers/failures are destroyed. Simpler sibling of atomic-forge's +# rent_race.sh (we need one short-lived box, not a race). +# +# env in : VAST_API_KEY, GPU_QUERY, SSH_KEY_FILE, [DISK_GB=40] [MAX_OFFERS=5] +# out : iid/host/port appended to $GITHUB_OUTPUT +set -euo pipefail + +DISK_GB="${DISK_GB:-40}" +MAX_OFFERS="${MAX_OFFERS:-5}" +IMAGE="nvidia/cuda:12.8.0-runtime-ubuntu22.04" + +vastai set api-key "$VAST_API_KEY" >/dev/null + +vastai search offers \ + "$GPU_QUERY disk_space>=$DISK_GB inet_down>=500 reliability>0.98 rentable=true" \ + -o dph --raw > offers.json +N=$(jq 'length' offers.json) +[ "$N" -gt 0 ] || { echo "::error::no vast offers match: $GPU_QUERY"; exit 1; } +echo "offers found: $N, trying up to $MAX_OFFERS cheapest" + +IID="" +cleanup() { [ -n "$IID" ] && vastai destroy instance "$IID" >/dev/null 2>&1 || true; } + +for i in $(seq 0 $((MAX_OFFERS - 1))); do + [ "$i" -lt "$N" ] || break + OFFER=$(jq -r ".[$i].id" offers.json) + DPH=$(jq -r ".[$i].dph_total" offers.json) + echo "--- offer $OFFER (\$${DPH}/h)" + if ! vastai create instance "$OFFER" --image "$IMAGE" \ + --disk "$DISK_GB" --ssh --direct --raw > create.json 2>&1; then + echo "create failed, next offer"; continue + fi + IID=$(jq -r '.new_contract // empty' create.json) + [ -n "$IID" ] || { echo "no contract id, next offer"; continue; } + + # created -> loading (image pull) -> running; give it 10 minutes + for tick in $(seq 1 40); do + ST=$(vastai show instance "$IID" --raw 2>/dev/null | jq -r '.actual_status // "?"') + [ "$ST" = "running" ] && break + sleep 15 + done + if [ "$ST" != "running" ]; then + echo "offer $OFFER never reached running ($ST), destroying" + cleanup; IID=""; continue + fi + + URL=$(vastai ssh-url "$IID") + HOST=$(echo "$URL" | sed -E 's|ssh://root@([^:]+):.*|\1|') + PORT=$(echo "$URL" | sed -E 's|.*:([0-9]+)$|\1|') + + # ssh может подняться на минуту позже статуса running — пробуем с ретраями + OK="" + for try in $(seq 1 8); do + if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ + -i "$SSH_KEY_FILE" -p "$PORT" root@"$HOST" 'echo ssh-ok' 2>/dev/null | grep -q ssh-ok; then + OK=1; break + fi + sleep 15 + done + if [ -z "$OK" ]; then + echo "offer $OFFER: ssh unreachable, destroying" + cleanup; IID=""; continue + fi + + echo "rented: iid=$IID $HOST:$PORT" + { echo "iid=$IID"; echo "host=$HOST"; echo "port=$PORT"; } >> "$GITHUB_OUTPUT" + exit 0 +done + +echo "::error::all offers failed" +exit 1 diff --git a/.github/workflows/backend-smoke.yml b/.github/workflows/backend-smoke.yml new file mode 100644 index 000000000000..4b8927eb1967 --- /dev/null +++ b/.github/workflows/backend-smoke.yml @@ -0,0 +1,128 @@ +# GPU smoke test of RELEASED backend archives on a rented vast.ai box. +# +# Not a required check by design: spot GPU rental is nondeterministic and +# costs money, so it runs on demand (button) and nightly — never as a PR +# gate. The result is posted as a NON-required commit status +# (gpu-smoke/) on the commit the tested release points at, so the +# dev -> master promotion PR shows the badge. +# +# What one run does (see .github/scripts/gpu-smoke/): +# rent cheapest matching GPU -> download the released archive -> quantize +# a tiny f16 model to NVFP4 with the SHIPPED llama-quantize -> serve it +# with -ngl 99 -> assert a coherent answer -> llama-bench -> assert the +# GPU backend actually did the work (a silent CPU fallback must FAIL). +# +# Secrets: VAST_API_KEY, VAST_SSH_KEY (private key registered with vast). + +name: GPU smoke (vast.ai) + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag to smoke' + default: 'dev-latest' + backends: + description: 'Backends to test (space-separated)' + default: 'linux-x64-vulkan linux-x64-cuda-13.3' + gpu_query: + description: 'vast.ai offer filter' + default: 'gpu_name=RTX_5090 num_gpus=1' + schedule: + # nightly against dev-latest; ~$1/night at current spot prices + - cron: '0 3 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.inputs.release_tag || 'dev-latest' }} + cancel-in-progress: false + +jobs: + smoke: + runs-on: ubuntu-22.04 + permissions: + contents: read + statuses: write + strategy: + fail-fast: false + matrix: + backend: ['linux-x64-vulkan', 'linux-x64-cuda-13.3'] + env: + RELEASE_TAG: ${{ github.event.inputs.release_tag || 'dev-latest' }} + BACKENDS: ${{ github.event.inputs.backends || 'linux-x64-vulkan linux-x64-cuda-13.3' }} + GPU_QUERY: ${{ github.event.inputs.gpu_query || 'gpu_name=RTX_5090 num_gpus=1' }} + + steps: + - name: Skip if backend not selected + id: gate + run: | + if echo "$BACKENDS" | grep -qw "${{ matrix.backend }}"; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + echo "backend ${{ matrix.backend }} not in '$BACKENDS' — skipping" + fi + + - name: Clone + if: steps.gate.outputs.run == 'true' + uses: actions/checkout@v6 + + - name: Install vast CLI + ssh key + if: steps.gate.outputs.run == 'true' + run: | + pip install -q vastai + install -m 600 /dev/null vast_key + printf '%s\n' "${{ secrets.VAST_SSH_KEY }}" > vast_key + + - name: Rent GPU box + if: steps.gate.outputs.run == 'true' + id: rent + env: + VAST_API_KEY: ${{ secrets.VAST_API_KEY }} + SSH_KEY_FILE: vast_key + run: bash .github/scripts/gpu-smoke/rent.sh + + - name: Run smoke on box + if: steps.gate.outputs.run == 'true' + id: run_smoke + env: + HOST: ${{ steps.rent.outputs.host }} + PORT: ${{ steps.rent.outputs.port }} + run: | + SSH="ssh -o StrictHostKeyChecking=no -o ConnectTimeout=20 -o ServerAliveInterval=30 -i vast_key -p $PORT root@$HOST" + scp -o StrictHostKeyChecking=no -i vast_key -P "$PORT" \ + .github/scripts/gpu-smoke/remote-smoke.sh root@"$HOST":/root/remote-smoke.sh + # vast hosts are known to drop long ssh sessions -> nohup + poll + $SSH "nohup bash /root/remote-smoke.sh '${{ matrix.backend }}' '$RELEASE_TAG' >/root/smoke.log 2>&1 &" + for i in $(seq 1 60); do + STATUS=$($SSH 'cat /root/smoke.status 2>/dev/null' 2>/dev/null || true) + [ -n "$STATUS" ] && break + sleep 20 + done + echo "===== smoke.log =====" + $SSH 'cat /root/smoke.log' 2>/dev/null || true + echo "=====================" + [ "$STATUS" = "OK" ] || { echo "::error::smoke failed for ${{ matrix.backend }}"; exit 1; } + + - name: Post commit status + if: always() && steps.gate.outputs.run == 'true' && steps.rent.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + SHA=$(gh api "repos/${{ github.repository }}/commits/$RELEASE_TAG" --jq .sha 2>/dev/null || true) + [ -n "$SHA" ] || { echo "cannot resolve $RELEASE_TAG to a commit, skipping status"; exit 0; } + STATE=failure + [ "${{ steps.run_smoke.outcome }}" = "success" ] && STATE=success + gh api "repos/${{ github.repository }}/statuses/$SHA" \ + -f state="$STATE" \ + -f context="gpu-smoke/${{ matrix.backend }}" \ + -f description="NVFP4 smoke on rented GPU ($RELEASE_TAG)" \ + -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + - name: Destroy GPU box + if: always() && steps.rent.outputs.iid != '' + env: + VAST_API_KEY: ${{ secrets.VAST_API_KEY }} + run: | + vastai set api-key "$VAST_API_KEY" >/dev/null + vastai destroy instance "${{ steps.rent.outputs.iid }}" || true + echo "destroyed instance ${{ steps.rent.outputs.iid }}" From 01aa67a7f2228da50131785829ef4362fea4855d Mon Sep 17 00:00:00 2001 From: worthant Date: Wed, 22 Jul 2026 05:49:25 +0300 Subject: [PATCH 7/7] smoke: fix CUDA device assert and tg128 column The CUDA device line in the bench log is 'Device 0: NVIDIA ...' with no 'cuda' on the same line, so the combined regex never matched and a fully working CUDA run (60k t/s pp512 on an RTX 5090) was reported as FAIL. Also the tg128 extraction read the test-name column instead of the t/s column. Both asserts validated against the real bench.log from that run. --- .github/scripts/gpu-smoke/remote-smoke.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/gpu-smoke/remote-smoke.sh b/.github/scripts/gpu-smoke/remote-smoke.sh index 5050a70ac92a..99c5d7c7e827 100755 --- a/.github/scripts/gpu-smoke/remote-smoke.sh +++ b/.github/scripts/gpu-smoke/remote-smoke.sh @@ -85,11 +85,12 @@ case "$BACKEND" in ;; linux-x64-cuda-13.3) grep -q "load_backend: loaded CUDA backend" bench.log || fail "CUDA backend not loaded" - grep -Eiq "cuda.*(NVIDIA|GeForce|RTX|H[0-9]+)" bench.log || fail "CUDA device is not an NVIDIA GPU" + # bench log format: " Device 0: NVIDIA GeForce RTX 5090, compute capability 12.0" + grep -Eq "Device [0-9]+: NVIDIA" bench.log || fail "CUDA device is not an NVIDIA GPU" ;; *) fail "unknown backend $BACKEND" ;; esac -TG=$(awk -F'|' '/tg128/ {gsub(/ /,"",$7); print $7}' bench.log | head -1) +TG=$(awk -F'|' '/tg128/ {gsub(/^ +| +$/,"",$8); split($8,a," "); print a[1]; exit}' bench.log) echo "tg128: ${TG:-?} t/s" echo "SMOKE OK: $BACKEND @ $TAG ($VERSION_LINE)"