From a6b354ebf1b1c86de1e4d6b018ea014ebd6bb2b9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 16 Sep 2026 12:23:34 +0000 Subject: [PATCH 1/2] Build a CUDA bundle for Windows on ARM Windows ARM64 hosts with an NVIDIA GPU are the only CUDA hosts we publish nothing for: the installer looks for a windows-arm64-cuda bundle, finds none, and falls back to ggml-org's single llama-bin-win-cuda-13.4-arm64.zip. Add the leg that produces it. It cross compiles ggml-cuda on an x64 runner with the amd64_arm64 MSVC toolset and CUDA 13.4 (the only Windows toolkit with ARM64 target libraries), then merges the backend into the arm64 CPU bundle the same run already builds. Same split upstream uses for its arm64 CUDA zip. --- .../unsloth-prebuilt-cuda-windows-arm64.yml | 391 ++++++++++++++++++ .github/workflows/unsloth-prebuilt.yml | 43 +- scripts/unsloth/assemble_metadata.py | 9 +- scripts/unsloth/package_bundle.py | 6 +- 4 files changed, 436 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml diff --git a/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml b/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml new file mode 100644 index 000000000000..1faf90589ce6 --- /dev/null +++ b/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml @@ -0,0 +1,391 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +name: "Unsloth prebuilt: CUDA Windows ARM64" + +# Reusable child of unsloth-prebuilt.yml. Produces the one Windows-on-ARM CUDA +# bundle (app--windows-arm64-cuda13-portable.zip) the installer looks for +# before it falls back to ggml-org's single llama-bin-win-cuda-13.4-arm64.zip. +# +# Why this is a separate child rather than an arm64 row in +# unsloth-prebuilt-cuda-windows.yml: +# +# * There is no ARM64 Windows runner with a CUDA toolkit. The leg cross +# compiles on an x64 runner with the amd64_arm64 MSVC toolset, the same way +# unsloth-prebuilt-cpu.yml already cross compiles the Windows arm64 CPU +# bundle, and the same way ggml-org's release.yml builds its arm64 CUDA zip. +# * Only CUDA 13.4 ships Windows ARM64 target libraries (cudart + cublas), and +# only from packages.nvidia.com rather than the redist CDN the x64 legs use, +# so it needs its own component list, not another row in the x64 table. +# * It builds one target (ggml-cuda), not the whole tool set: everything else +# in the bundle comes from the arm64 CPU bundle this run already builds. +# That is exactly upstream's arrangement -- their arm64 CUDA job packs +# ggml-cuda.dll alone and the release merges it into the arm64 CPU zip -- +# and it keeps the leg down to one backend's compile instead of a full +# cross-compiled tree with BoringSSL, the server and the tools. +# +# Because of that last point the job does not use the shared +# .github/actions/unsloth-cuda-windows-setup composite: that action exists so +# the x64 `warm` shards and `build` job configure byte-identically and share +# ccache entries, and there is one job here. +# +# The arm64 CPU bundle is a sibling job in the same run, not a `needs:`. The +# parent's "Wait for the build matrix" step fails a publish if a child has no +# job record within 45 minutes of the run starting, and a `needs:` here would +# hold this job's record back until the whole CPU child finished. So the leg +# starts immediately, compiles for an hour or more, and only then waits for the +# artifact -- which by that point has long since been uploaded. + +on: + workflow_call: + inputs: + tag: + description: 'Upstream llama.cpp release tag (b####), resolved by parent' + required: true + type: string + commit: + description: 'Upstream commit SHA for that tag, resolved by parent' + required: true + type: string + repo: + description: 'Source repo (owner/name): ggml-org/llama.cpp for plain builds, or this repo for mix tags' + required: false + default: 'ggml-org/llama.cpp' + type: string + source_artifact: + description: 'Workflow artifact (app-source-*) holding the stamped source tree; set by resolve for every build' + required: false + default: '' + type: string + + secrets: + AZURE_CLIENT_ID: + description: 'Azure Trusted Signing client id; empty on forks, which then build unsigned' + required: false + AZURE_CLIENT_SECRET: + description: 'Azure Trusted Signing client secret' + required: false + AZURE_TENANT_ID: + description: 'Azure Trusted Signing tenant id' + required: false + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: + description: 'Azure Trusted Signing account name' + required: false + AZURE_CERTIFICATE_PROFILE_NAME: + description: 'Azure Trusted Signing certificate profile name' + required: false + +permissions: + contents: read + # The CPU bundle wait below reads this run's artifact list. + actions: read + +env: + # One profile, and it is not a copy of an x64 row. Windows on ARM CUDA hosts + # are Blackwell only -- NVIDIA's ARM64 Windows driver and its 13.4 toolkit + # target the GB10 class parts (sm_121) and discrete RTX 50 (sm_120); there is + # no Windows ARM64 machine with a Pascal, Turing, Ampere, Ada or Hopper GPU + # to cover. Building those archs anyway would double a cross-compiled leg's + # runtime to ship PTX nothing can load. Linux arm64 keeps 90/100 because + # Grace-Hopper and GB200 servers are real there. + PROFILE: cuda13-portable + LINE: cuda13 + KLASS: portable + RANK: '60' + CUDA: '13.4' + ARCHS: '120 121' + +jobs: + build: + name: arm64/cuda13-portable + runs-on: windows-2022 + # Same reasoning as the x64 child: this is the only job that reads the + # Azure Trusted Signing secrets, and naming an environment is what makes + # them gateable. Inert until protection rules exist. + environment: release-signing + # Hang guard, below assemble's 350 and the waiter's 330-minute deadline. + # Two archs of one backend, so this is far under the x64 legs' ceiling; + # the cap is here to stop a wedged leg holding a runner, not to fit a + # measured ceiling that does not exist yet. + timeout-minutes: 300 + defaults: + run: + shell: pwsh + steps: + - name: Checkout build tooling (this repo) + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + path: tooling + + - name: Download source @ ${{ inputs.tag }} + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: ${{ inputs.source_artifact }} + path: srcpkg + + - name: Extract source + shell: bash + run: | + set -eux + mkdir -p src + tar -xzf "srcpkg/llama.cpp-source-${{ inputs.tag }}.tar.gz" -C src --strip-components=1 + test -f src/cmake/arm64-windows-msvc-cuda.cmake + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.11" + + - name: Check Ninja + run: | + (Get-Command ninja.exe -ErrorAction Stop).Source + ninja --version + + # amd64_arm64: x64 host binaries emitting ARM64 code. The toolchain file + # picks cl.exe out of Hostx64/arm64 for nvcc's host pass, so this has to + # be the cross toolset, not the native one. + - name: Setup MSVC (amd64_arm64) + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 + with: + arch: amd64_arm64 + + # The x64 legs install from the redist CDN, which has no ARM64 target + # libraries at any version. 13.4 is the first toolkit that ships them and + # it lives in the bin-archive pool instead; the host-side pieces (nvcc, + # crt, nvvm, cccl) stay x86_64 because the compiler runs on this runner. + # Same component set and same pool ggml-org's release.yml installs. + - name: Install CUDA toolkit 13.4 (x86_64 host, ARM64 target) + run: | + $ErrorActionPreference = 'Stop' + $pool = 'https://packages.nvidia.com/bin-archive/pool' + $guid = '5B515474-7E78-11F1-8656-C51E4F4B317F' + $components = @( + "windows-x86_64/$guid/cccl-windows-x86_64-13.3.4.1.2-archive", + "windows-x86_64/$guid/cuda_crt-windows-x86_64-13.4.46-archive", + "windows-x86_64/$guid/cuda_nvcc-windows-x86_64-13.4.46-archive", + "windows-x86_64/$guid/libnvvm-windows-x86_64-13.4.46-archive", + "windows-arm64/$guid/cuda_cudart-windows-arm64-13.4.46-archive", + "windows-arm64/$guid/libcublas-windows-arm64-13.7.0.10-archive" + ) + $prefix = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v${{ env.CUDA }}" + $stage = Join-Path $env:RUNNER_TEMP 'cuda-redist' + New-Item -ItemType Directory -Force -Path $prefix, $stage | Out-Null + foreach ($component in $components) { + $name = Split-Path $component -Leaf + $zip = Join-Path $stage "$name.zip" + curl.exe -fsSL --retry 3 -o $zip "$pool/$component.zip" + if ($LASTEXITCODE -ne 0) { throw "download failed: $component" } + tar.exe -xf $zip -C $stage + if ($LASTEXITCODE -ne 0) { throw "extract failed: $name" } + robocopy (Join-Path $stage $name) $prefix /E /NFL /NDL /NJH /NJS /NP | Out-Null + if ($LASTEXITCODE -ge 8) { throw "copy failed: $name ($LASTEXITCODE)" } + Remove-Item -Recurse -Force $zip, (Join-Path $stage $name) + } + "$prefix\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + "CUDA_PATH=$prefix" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "CUDA_HOME=$prefix" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "CUDA_PATH_V13_4=$prefix" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + exit 0 + + # The toolchain file points CMake straight at lib/arm64; if the archive + # ever stops shipping it, fail here with the reason rather than in a + # CMake error that reads like a missing CUDA install. + - name: Verify the toolkit has ARM64 target libraries + run: | + nvcc --version + $libs = "$env:CUDA_PATH\lib\arm64" + if (-not (Test-Path $libs)) { throw "no ARM64 target libraries under $libs" } + foreach ($lib in @('cudart.lib', 'cublas.lib', 'cublasLt.lib')) { + if (-not (Test-Path (Join-Path $libs $lib))) { throw "missing $lib in $libs" } + } + Get-ChildItem $libs | Select-Object -ExpandProperty Name + + - name: ccache + uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 + with: + key: cuda-${{ env.CUDA }}-windows-arm64-${{ env.PROFILE }}-${{ inputs.tag }} + restore-keys: | + cuda-${{ env.CUDA }}-windows-arm64-${{ env.PROFILE }} + append-timestamp: false + variant: ccache + max-size: 3G + save: false + + - name: Configure ccache + run: | + ccache --set-config=compiler_check=content + ccache --set-config=sloppiness=include_file_mtime,include_file_ctime,pch_defines,locale + ccache --set-config=compression_level=6 + ccache -z + + # GGML_CPU=OFF: the CPU backend, the server and the tools come from the + # arm64 CPU bundle, which is built with clang (upstream's proven Windows + # arm64 CPU toolchain) rather than cross MSVC. GGML_BACKEND_DL=ON is what + # makes that split legal -- ggml loads ggml-cuda.dll through the backend + # registry, a C ABI, so the two compilers never share a C++ interface. + - name: Configure + working-directory: src + run: | + $archs = "${{ env.ARCHS }}".Replace(' ', ';') + cmake -S . -B build -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-msvc-cuda.cmake ` + -DGGML_NATIVE=OFF ` + -DGGML_BACKEND_DL=ON ` + -DGGML_CPU=OFF ` + -DGGML_CUDA=ON ` + -DGGML_CUDA_CUB_3DOT2=ON ` + -DLLAMA_BUILD_TESTS=OFF ` + -DLLAMA_BUILD_EXAMPLES=OFF ` + -DLLAMA_BUILD_TOOLS=OFF ` + -DLLAMA_BUILD_SERVER=OFF ` + -DCMAKE_CUDA_ARCHITECTURES="$archs" ` + -DCMAKE_C_COMPILER_LAUNCHER=ccache ` + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ` + -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Build ggml-cuda + working-directory: src + run: | + # -j 3 for the same reason as the x64 legs: 4 vCPU / 16 GB runners, + # and nvcc peaks around 3 GB of host RSS per translation unit. + cmake --build build --config Release -j 3 --target ggml-cuda + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + # Nothing downstream would notice an x64 ggml-cuda.dll until a user's ARM64 + # host refused to load it, so check the PE machine field here. + - name: Check the backend is ARM64 + run: | + $dll = 'src/build/bin/ggml-cuda.dll' + if (-not (Test-Path $dll)) { throw "the build produced no $dll" } + $stream = [System.IO.File]::OpenRead((Resolve-Path $dll)) + try { + $reader = New-Object System.IO.BinaryReader($stream) + $stream.Position = 0x3C + $stream.Position = $reader.ReadInt32() + 4 + $machine = $reader.ReadUInt16() + } finally { $stream.Dispose() } + "ggml-cuda.dll machine 0x{0:X4}" -f $machine + if ($machine -ne 0xAA64) { throw "ggml-cuda.dll is not an ARM64 image (machine 0x$('{0:X4}' -f $machine))" } + + - name: ccache stats + if: ${{ always() }} + shell: bash + run: | + ccache -s -v + { + echo "### ccache windows arm64/${PROFILE}" + echo "" + echo '```' + ccache -s + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Sign only what this leg built. Everything else in the finished bundle + # was signed by the CPU leg that produced it, and re-signing a signed + # image is wasted signing quota. + - name: Stage the backend for signing + run: | + New-Item -ItemType Directory -Force -Path cudabin | Out-Null + Copy-Item src/build/bin/ggml-cuda.dll cudabin/ + + - name: Sign Windows binaries + id: sign_windows + uses: ./tooling/.github/actions/sign-windows + with: + path: cudabin + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-account: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + azure-certificate-profile: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} + + # Sibling job, not a `needs:` (see the header). Poll rather than assume: + # on a warm ccache this leg can finish before the CPU child does. + - name: Wait for the Windows ARM64 CPU bundle + env: + GH_TOKEN: ${{ github.token }} + ARTIFACT: app-${{ inputs.tag }}-windows-arm64-cpu + shell: bash + run: | + set -uo pipefail + DEADLINE=$(( $(date +%s) + 90 * 60 )) + while :; do + if NAMES="$(gh api --paginate \ + "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \ + --jq '.artifacts[].name' 2>/dev/null)"; then + if printf '%s\n' "$NAMES" | grep -qxF "$ARTIFACT"; then + echo "${ARTIFACT} is available" + exit 0 + fi + fi + if [ "$(date +%s)" -gt "$DEADLINE" ]; then + echo "ERROR: ${ARTIFACT} never appeared; the Windows ARM64 CPU leg must succeed before this bundle can be packed" >&2 + exit 1 + fi + echo "waiting for ${ARTIFACT}" + sleep 60 + done + + - name: Download the Windows ARM64 CPU bundle + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: app-${{ inputs.tag }}-windows-arm64-cpu + path: cpubundle + + # The CPU zip is the whole build/bin/Release tree plus the OpenMP runtime. + # Dropping the backend in beside it gives package_bundle.py the same shape + # of input directory a native build tree would have. + - name: Compose the bundle input + run: | + $zip = "cpubundle/app-${{ inputs.tag }}-windows-arm64-cpu.zip" + if (-not (Test-Path $zip)) { throw "the CPU leg's artifact does not contain $zip" } + Expand-Archive -Path $zip -DestinationPath bin -Force + Copy-Item cudabin/ggml-cuda.dll bin/ -Force + foreach ($required in @('llama-server.exe', 'ggml-base.dll', 'ggml-cpu.dll')) { + if (-not (Test-Path (Join-Path 'bin' $required))) { throw "the CPU bundle is missing $required" } + } + Get-ChildItem bin | Select-Object -ExpandProperty Name + + - name: Package bundle + env: + PLATFORM: windows + ARCH: arm64 + BIN_DIR: ${{ github.workspace }}/bin + SRC_DIR: ${{ github.workspace }}/src + OUT_DIR: ${{ github.workspace }}/dist + TAG: ${{ inputs.tag }} + SOURCE_COMMIT: ${{ inputs.commit }} + SOURCE_REPO: ${{ inputs.repo }} + SOURCE_REF_KIND: ${{ inputs.repo == 'ggml-org/llama.cpp' && 'tag' || 'mix' }} + PROFILE: ${{ env.PROFILE }} + LINE: ${{ env.LINE }} + KLASS: ${{ env.KLASS }} + RANK: ${{ env.RANK }} + TOOLKIT_LINE: ${{ env.CUDA }} + DOCKER_IMAGE: github-hosted windows-2022 cross-compiled to arm64, CUDA ${{ env.CUDA }} (NVIDIA bin-archive) + ARCHS: ${{ env.ARCHS }} + run: python tooling/scripts/unsloth/package_bundle.py + + - name: Verify every binary in the bundle is signed + if: ${{ steps.sign_windows.outputs.signed == 'true' }} + run: tooling/.github/scripts/assert-windows-bundle-signed.ps1 -Path 'dist/app-${{ inputs.tag }}-windows-arm64-${{ env.PROFILE }}.zip' + + - name: Upload bundle artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: app-${{ inputs.tag }}-windows-arm64-${{ env.PROFILE }} + path: dist/app-${{ inputs.tag }}-windows-arm64-${{ env.PROFILE }}.zip + if-no-files-found: error + + - name: Evict stale ccache files + if: ${{ !cancelled() }} + continue-on-error: true + run: ccache --evict-older-than 7d + + - name: Save ccache + if: ${{ always() }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ github.workspace }}\.ccache + key: ccache-cuda-${{ env.CUDA }}-windows-arm64-${{ env.PROFILE }}-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/.github/workflows/unsloth-prebuilt.yml b/.github/workflows/unsloth-prebuilt.yml index 2a640f13fc05..4aeea669ef60 100644 --- a/.github/workflows/unsloth-prebuilt.yml +++ b/.github/workflows/unsloth-prebuilt.yml @@ -4,14 +4,15 @@ name: Unsloth prebuilt (full release) # Atomic daily build for unslothai/llama.cpp. One cron, one workflow run, one -# release per upstream b#### tag. Splits the heavy build work into six reusable +# release per upstream b#### tag. Splits the heavy build work into seven reusable # children: -# unsloth-prebuilt-cuda.yml -- Linux CUDA bundles (x64 + arm64, matrix profiles) -# unsloth-prebuilt-cuda-windows.yml -- CUDA Windows bundles (x64, matrix profiles) -# unsloth-prebuilt-rocm.yml -- ROCm bundles (Windows + Ubuntu, per gfx target) -# unsloth-prebuilt-macos.yml -- macOS bundles (arm64 Metal + x64 CPU) -# unsloth-prebuilt-cpu.yml -- CPU-only bundles (Linux + Windows, x64 + arm64) -# unsloth-prebuilt-vulkan.yml -- Vulkan bundles (Linux x64/arm64 + Windows x64) +# unsloth-prebuilt-cuda.yml -- Linux CUDA bundles (x64 + arm64, matrix profiles) +# unsloth-prebuilt-cuda-windows.yml -- CUDA Windows bundles (x64, matrix profiles) +# unsloth-prebuilt-cuda-windows-arm64.yml -- CUDA Windows bundle (arm64, cross-compiled) +# unsloth-prebuilt-rocm.yml -- ROCm bundles (Windows + Ubuntu, per gfx target) +# unsloth-prebuilt-macos.yml -- macOS bundles (arm64 Metal + x64 CPU) +# unsloth-prebuilt-cpu.yml -- CPU-only bundles (Linux + Windows, x64 + arm64) +# unsloth-prebuilt-vulkan.yml -- Vulkan bundles (Linux x64/arm64 + Windows x64) # # Atomicity: the `assemble` job depends on all children. GitHub's default # `needs` semantics require all needs to succeed -- if any matrix entry in @@ -573,6 +574,28 @@ jobs: warm_matrix: ${{ needs.resolve.outputs.win_cuda_warm_matrix }} secrets: inherit + # Windows on ARM CUDA. Separate child, not a row in the x64 matrix: it cross + # compiles on an x64 runner with the only toolkit that has ARM64 target + # libraries (13.4) and builds just the ggml-cuda backend, which it merges into + # the arm64 CPU bundle the `CPU` child builds in this same run. See that + # workflow's header. + build-windows-cuda-arm64: + name: CUDA Windows ARM64 + needs: resolve + if: ${{ needs.resolve.outputs.exists != 'true' || github.event_name == 'workflow_dispatch' }} + # actions: read is for the step that waits on the CPU child's artifact; a + # called workflow cannot request more than the caller's job grants it. + permissions: + contents: read + actions: read + uses: ./.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml + with: + tag: ${{ needs.resolve.outputs.tag }} + repo: ${{ needs.resolve.outputs.repo }} + source_artifact: ${{ needs.resolve.outputs.source_artifact }} + commit: ${{ needs.resolve.outputs.commit }} + secrets: inherit + # First of two ccache prunes (see scripts/unsloth/prune_ccache.sh): one generation per prefix # at run start so old + new fit under the limit. Never gates the waiter, which excludes it by name. prune-ccache: @@ -688,7 +711,7 @@ jobs: # job list as " / ", e.g. # "CUDA / x64/cuda12-legacy". One prefix per build child; every one of # them must be represented or we are not looking at a complete run. - PREFIXES=('CUDA / ' 'CUDA Windows / ' 'ROCm / ' 'macOS / ' 'CPU / ' 'Vulkan / ') + PREFIXES=('CUDA / ' 'CUDA Windows / ' 'CUDA Windows ARM64 / ' 'ROCm / ' 'macOS / ' 'CPU / ' 'Vulkan / ') SELF='Assemble metadata + publish' ALERT='Report pipeline health' # Best-effort jobs that never gate a publish: the start-of-run ccache prune and the @@ -1114,7 +1137,7 @@ jobs: # Every build leg, not just assemble. # assemble needs only resolve, so one failing leg fails it immediately while slower legs are still building, and with always() this job then deleted the app-source-* artifact out from under them. # On 08-27 one arm64 CPU failure became ten: nine ROCm legs died on "Artifact not found" seconds later, which reads as a ROCm fault and is not. - needs: [resolve, build-cuda, build-windows-cuda, build-rocm, build-macos, build-cpu, build-vulkan, assemble] + needs: [resolve, build-cuda, build-windows-cuda, build-windows-cuda-arm64, build-rocm, build-macos, build-cpu, build-vulkan, assemble] # always(), so a run that publishes NOTHING still cleans up after itself. # Gating this on `published` leaked every non-publishing run's bundles: a # workflow_dispatch defaults to publish:false, and a cancelled run never @@ -1288,7 +1311,7 @@ jobs: # Runs on publish-intent runs only, so subset test dispatches stay quiet. alert: name: Report pipeline health - needs: [resolve, build-cuda, build-windows-cuda, build-rocm, build-macos, build-cpu, build-vulkan, assemble] + needs: [resolve, build-cuda, build-windows-cuda, build-windows-cuda-arm64, build-rocm, build-macos, build-cpu, build-vulkan, assemble] if: ${{ always() && (github.event_name == 'schedule' || inputs.publish) }} runs-on: ubuntu-24.04 permissions: diff --git a/scripts/unsloth/assemble_metadata.py b/scripts/unsloth/assemble_metadata.py index 87fa3cebe1e4..fcc83e4eef71 100644 --- a/scripts/unsloth/assemble_metadata.py +++ b/scripts/unsloth/assemble_metadata.py @@ -71,6 +71,7 @@ ("linux", "x64"): {"manifest": "linux-cuda", "sha": "linux-cuda-app"}, ("linux", "arm64"): {"manifest": "linux-arm64-cuda", "sha": "linux-arm64-cuda-app"}, ("windows", "x64"): {"manifest": "windows-cuda", "sha": "windows-cuda-app"}, + ("windows", "arm64"): {"manifest": "windows-arm64-cuda", "sha": "windows-arm64-cuda-app"}, } KIND_BY_ROCM_PLATFORM = { @@ -434,7 +435,11 @@ def scan_bundles(regex) -> list[tuple[str, "re.Match[str]"]]: assets = upstream_assets(tag, token) wanted: list[tuple[str, str]] = [] # (name, kind) for name in sorted(assets): - if re.fullmatch(r"cudart-llama-bin-win-cuda-\d+\.\d+-x64\.zip", name): + # arm64 as well as x64: package_bundle.py deliberately leaves the CUDA + # runtime out of every bundle, so the Windows ARM64 CUDA bundle needs + # upstream's cudart-...-arm64.zip recorded here or the installer has no + # approved hash to pair it with. + if re.fullmatch(r"cudart-llama-bin-win-cuda-\d+\.\d+-(?:x64|arm64)\.zip", name): wanted.append((name, "windows-cuda-upstream")) # The win-cuda BINARY zips must be recorded under their own names too: # the installer resolves an attempt's hash by exact asset name first @@ -442,7 +447,7 @@ def scan_bundles(regex) -> list[tuple[str, "re.Match[str]"]]: # entries every Windows CUDA binary gets paired with the cudart digest # and fails download verification. elif re.fullmatch( - rf"llama-{re.escape(tag)}-bin-win-cuda-\d+\.\d+-x64\.zip", name + rf"llama-{re.escape(tag)}-bin-win-cuda-\d+\.\d+-(?:x64|arm64)\.zip", name ): wanted.append((name, "windows-cuda-upstream")) # x64 CPU and all current Vulkan targets are no longer passthroughs -- diff --git a/scripts/unsloth/package_bundle.py b/scripts/unsloth/package_bundle.py index dca4ccee9d11..4bd2dc9fba66 100644 --- a/scripts/unsloth/package_bundle.py +++ b/scripts/unsloth/package_bundle.py @@ -141,7 +141,11 @@ class WindowsStrategy(PlatformStrategy): # No portable readelf/otool equivalent; the project's own DLLs live beside # the binaries in build/bin/Release, so bundle those by name convention. - LOCAL_DLL_PREFIXES = ("ggml", "llama", "mtmd") + # libomp: the Windows arm64 CPU bundle is a clang build with GGML_OPENMP=ON + # and carries the VS LLVM redist's libomp140.aarch64.dll beside the binaries. + # ggml-cpu.dll imports it by name, so a curated bundle built from that tree + # (the arm64 CUDA leg) has to keep it or nothing in the bundle loads. + LOCAL_DLL_PREFIXES = ("ggml", "llama", "mtmd", "libomp") def local_needed(self, path: Path, bin_dir: Path) -> list[str]: return [ From 1ab879a8aa4223d9cfacc23c848a12e169502960 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 16 Sep 2026 13:09:59 +0000 Subject: [PATCH 2/2] Quote the toolchain path in the pwsh configure line --- .github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml b/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml index 1faf90589ce6..ece6c6287180 100644 --- a/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml +++ b/.github/workflows/unsloth-prebuilt-cuda-windows-arm64.yml @@ -228,7 +228,7 @@ jobs: $archs = "${{ env.ARCHS }}".Replace(' ', ';') cmake -S . -B build -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` - -DCMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-msvc-cuda.cmake ` + '-DCMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-msvc-cuda.cmake' ` -DGGML_NATIVE=OFF ` -DGGML_BACKEND_DL=ON ` -DGGML_CPU=OFF `