Skip to content

Build a CUDA bundle for Windows on ARM - #218

Merged
danielhanchen merged 2 commits into
masterfrom
woa-cuda-prebuilt
Sep 16, 2026
Merged

danielhanchen merged 2 commits into
masterfrom
woa-cuda-prebuilt

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Windows on ARM is the only CUDA platform this repo publishes nothing for. install_llama_prebuilt.py already looks for a windows-arm64-cuda bundle first and only then falls back to ggml-org's llama-bin-win-cuda-13.4-arm64.zip, so on a WoA machine with an NVIDIA GPU we currently hand the user upstream's build and give up three things we give every other CUDA host:

  • SM-targeted selection. Our bundles carry coverage metadata so the installer picks the tightest build for the actual GPU. Upstream ships one zip for every card.
  • The Unsloth patch set. A mix tag's PRs are not in upstream's binary.
  • Signature verification in CI. Our Windows bundles go through assert-windows-bundle-signed.ps1; upstream's are unsigned.

This adds the leg that produces it.

Design

There is no ARM64 Windows runner with a CUDA toolkit, so the leg cross compiles on an x64 runner with the amd64_arm64 MSVC toolset. That is the same approach unsloth-prebuilt-cpu.yml already uses for the Windows arm64 CPU bundle, and the same one ggml-org's release.yml uses for its arm64 CUDA zip.

It builds only ggml-cuda, against cmake/arm64-windows-msvc-cuda.cmake with GGML_CPU=OFF, then merges the resulting ggml-cuda.dll into the arm64 CPU bundle this same run already builds, and repacks through package_bundle.py. That is upstream's arrangement too: their arm64 CUDA job packs ggml-cuda.dll alone and the release merges it into the arm64 CPU zip. GGML_BACKEND_DL=ON on both sides is what makes the split legal, since ggml loads the backend through the registry, a C ABI, so the clang-built CPU tree and the MSVC-built backend never share a C++ interface.

Toolkit comes from packages.nvidia.com/bin-archive, not the redist CDN the x64 legs use: 13.4 is the first Windows toolkit shipping ARM64 target libraries and it is only in that pool. Host-side pieces (nvcc, crt, nvvm, cccl) stay x86_64 because the compiler runs on the x64 runner; only cuda_cudart and libcublas are the arm64 builds. Same component set ggml-org installs.

Why a separate child workflow

Four things in this leg differ from every row of the x64 matrix: the toolkit source and version, the toolset, the single build target, and the packaging, which is a merge rather than a curate from a build tree. Folding those into unsloth-prebuilt-cuda-windows.yml would mean conditionals on all four plus arch in every ccache key, with a one-time cache miss for the x64 legs. A separate child keeps the x64 path byte for byte unchanged.

It also does not use the shared unsloth-cuda-windows-setup composite. That action exists so the x64 warm shards and build job configure identically and share ccache entries; there is one job here, so there is nothing to keep in sync.

The arm64 CPU bundle is a sibling job, 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: would hold this job's record back until the whole CPU child finished. So the leg starts immediately, compiles, and only then polls for the artifact, which by that point has long since been uploaded.

Why sm_120 and sm_121 only

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, and building those archs would roughly double a cross-compiled leg's runtime to ship PTX nothing can load. Linux arm64 keeps 90 and 100 because Grace-Hopper and GB200 servers are real there.

Staging evidence

Proved the toolchain on a staging run before proposing it: https://github.com/danielhanchen/unsloth-staging-2/actions/runs/35100180000

Same toolkit install, same toolchain file, same configure line, same arch list. Results:

-- Check for working C compiler: .../MSVC/14.44.35207/bin/Hostx64/arm64/cl.exe
-- CMAKE_SYSTEM_PROCESSOR: arm64
   VSCMD_ARG_TGT_ARCH: arm64

ggml-base.dll: machine 0xAA64
ggml-cuda.dll: machine 0xAA64

Configure 48 seconds, ggml-cuda build 26.7 minutes for both archs on a 4 vCPU runner with a cold cache, 77 MB artifact. The workflow re-checks the PE machine field itself and fails the job if ggml-cuda.dll is not an ARM64 image, so an x64 backend can never reach a bundle.

Other changes

  • assemble_metadata.py: ("windows", "arm64") added to KIND_BY_CUDA as windows-arm64-cuda, which is exactly the install_kind the installer asks for. The bundle filename regex already accepted windows plus arm64.
  • Same file: the upstream cudart and win-cuda binary regexes widened from -x64 to (?:x64|arm64). package_bundle.py deliberately leaves the CUDA runtime out of every bundle, so without this the arm64 bundle has no approved hash for the runtime it needs to pair with.
  • package_bundle.py: libomp added to the Windows local-DLL prefixes. The arm64 CPU bundle is a clang build with GGML_OPENMP=ON and carries libomp140.aarch64.dll; ggml-cpu.dll imports it by name, so curating a bundle from that tree without it produces an archive where nothing loads. No effect on the x64 legs, which are MSVC builds with no libomp.
  • Parent: new build-windows-cuda-arm64 job, added to the waiter's PREFIXES and to the reclaim and alert needs lists. It requests actions: read for the artifact poll, which a called workflow cannot grant itself.

What this does not prove

The staging run built the backend and checked its machine type; it did not run it, because no CI runner has a Windows ARM64 machine with an NVIDIA GPU. The first real bundle needs to be exercised on actual hardware before anyone should trust the selector's choice of it. The merge and packaging path is also unexercised until this runs for the first time in a full release, since the staging probe stopped after the build.

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.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T14:02:35.347728Z 1ab879a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@danielhanchen
danielhanchen merged commit 79bb6ca into master Sep 16, 2026
0 of 4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ab879a8aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +374 to +379
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the ARM64 CUDA archive before publishing

Add app-${TAG}-windows-arm64-cuda13-portable.zip to the parent workflow’s “Verify full bundle coverage before publish” list. That gate explicitly handles cases where all child jobs and uploads are green but download-artifact omits an individual bundle; because this newly uploaded archive is not checked there, such an anomaly would publish a release and manifest without the Windows ARM64 CUDA bundle, silently restoring the fallback behavior this change is intended to eliminate.

Useful? React with 👍 / 👎.

Comment on lines +442 to 443
if re.fullmatch(r"cudart-llama-bin-win-cuda-\d+\.\d+-(?:x64|arm64)\.zip", name):
wanted.append((name, "windows-cuda-upstream"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Index ARM64 cudart for mix releases

For normal scheduled mix releases, where the PR set is nonempty, is_upstream_release is false and the entire block containing this widened regex is skipped. The new local Windows ARM64 bundle is still advertised, but package_bundle.py deliberately excludes its CUDA runtime, so the release’s SHA index contains no approved ARM64 cudart hash and the installer cannot verify the runtime it must pair with the bundle. Fetch and index the base tag’s ARM64 cudart asset for mix builds as well.

Useful? React with 👍 / 👎.

Comment on lines +582 to +585
build-windows-cuda-arm64:
name: CUDA Windows ARM64
needs: resolve
if: ${{ needs.resolve.outputs.exists != 'true' || github.event_name == 'workflow_dispatch' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor only_profile before launching the ARM64 build

On every manual dispatch this condition launches the fixed cuda13-portable ARM64 child regardless of only_profile. A dispatch intended to test another profile, such as cuda12-legacy, therefore also performs this unrelated Windows ARM64 compilation and can fail the whole subset run if that leg fails; the existing Linux and Windows CUDA matrices instead filter all rows by the requested profile. Gate this job to only_profile == 'all' or only_profile == 'cuda13-portable'.

Useful? React with 👍 / 👎.

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