Add Docker build for Blackwell that runs on any NVIDIA GPU host
Adds a multi-stage Dockerfile producing an image that works on Ampere through Blackwell (sm_80 through sm_120: A100, RTX 30/40, H100, B100/B200, RTX 50-series, RTX 6000 Pro Blackwell). The build itself requires no GPU at all and runs on a free GitHub-hosted ubuntu-latest runner. How the GPU-less build works: 1. cu128 PyTorch wheels are fat binaries. torch._C._cuda_getArchFlags() returns 'sm_70 sm_75 sm_80 sm_86 sm_90 sm_100 sm_120' regardless of which GPU compiled the image, because the wheels are cross-compiled upstream by the PyTorch team. 2. All deps resolve in a single uv pip install pass with explicit pins (torch==2.10.0, --extra-index-url cu128, no --torch-backend=auto, no install.sh). This prevents the silent cu cascade where bitsandbytes' transitive cuda-toolkit==13 dep upgrades torch to 2.12+cu130 in a later resolver pass, leaving xformers and other cu128 wheels stranded. 3. Build-time verification uses package metadata (importlib.metadata.version) and the raw torch._C._cuda_getArchFlags() accessor. We deliberately avoid import unsloth at build time because unsloth.__init__ calls torch.cuda.get_device_properties(0), which requires an actual CUDA device and is not bypassable. Import-time correctness is exercised at deploy time by smoke_test.py with --gpus all. 4. UNSLOTH_COMPILE_DISABLE=1 and CUDA_VISIBLE_DEVICES="" during the build stage prevent any code path from JIT-compiling kernels for the build host's compute capability and baking the resulting cache into the image. The deploy GPU produces its own cache on first use. Other notes: - --index-strategy unsafe-best-match is needed because the PyTorch wheel index serves an old requests==2.28.1 that conflicts with datasets>=2.32.2, which the default first-index-wins strategy rejects. - Extra is cu128-ampere-torch2100 (ampere precedes the torch version in the pyproject ordering). - No flash-attn in the base image. FA3 is hard-refused on Blackwell upstream and unsloth gracefully falls back to xformers + SDPA. Users on Ampere / Ada / Hopper who want FA2 can pip install flash-attn on top. - Two stages: nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04 for the build, -cudnn-runtime for the deploy image. No nvcc in the published image. - A lockfile is emitted at /opt/unsloth-venv/requirements.lock.txt inside the image and can be extracted with docker/freeze.sh for byte-identical rebuilds even after PyPI moves on. CI workflow .github/workflows/docker-publish.yml: - Builds on ubuntu-latest on every push to main, every tag, weekly via cron, and manually via workflow_dispatch. Pushes to docker.io/unsloth/unsloth with cache via type=gha. - Optional smoke-test job runs on a self-hosted GPU runner if vars.HAS_GPU_RUNNER is set; skipped otherwise. End-to-end verification on sm_120 hardware is a nice-to-have, not a publish blocker. Validation: - Install path validated on a B200 host with CUDA_VISIBLE_DEVICES="" set (simulating the GPU-less CI runner): torch 2.10.0+cu128 holds, xformers 0.0.34, bitsandbytes 0.49.2, triton 3.6.0, transformers 5.5.0, trl 0.24.0, peft 0.19.1, accelerate 1.13.0. Arch flags include sm_100 and sm_120. - Runtime path validated end-to-end on B200: smoke_test.py imports unsloth, loads Llama-3.2-1B-Instruct-bnb-4bit in 4-bit, completes 5 LoRA steps with loss decreasing 4.11 -> 3.75. xformers fallback active as designed. Files: - docker/Dockerfile multi-stage cu128 build - docker/build.sh local build wrapper - docker/freeze.sh extract lockfile from a built image - docker/smoke_test.py runtime verification, run with --gpus all - docker/.dockerignore - .github/workflows/docker-publish.yml
This commit is contained in:
parent
56e9046b2f
commit
c6d92160f6
6 changed files with 530 additions and 0 deletions
118
.github/workflows/docker-publish.yml
vendored
Normal file
118
.github/workflows/docker-publish.yml
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Builds and publishes the Blackwell-compatible Unsloth Docker image.
|
||||
#
|
||||
# The build runs on a free GitHub-hosted Ubuntu runner with NO GPU attached.
|
||||
# This is possible because:
|
||||
# 1. cu128 PyTorch wheels are fat binaries -- they already ship sm_70 through
|
||||
# sm_120 SASS, cross-compiled upstream by the PyTorch team.
|
||||
# 2. The Dockerfile pins explicit wheel URLs (no --torch-backend=auto, no
|
||||
# install.sh that introspects the host driver).
|
||||
# 3. The build-time sanity check uses torch._C._cuda_getArchFlags(), which
|
||||
# reads compiled wheel metadata and does NOT require a CUDA device.
|
||||
# 4. UNSLOTH_COMPILE_DISABLE=1 prevents Unsloth from JIT-compiling a Triton
|
||||
# kernel cache keyed to the (non-existent) build-host GPU.
|
||||
#
|
||||
# Required repository secrets:
|
||||
# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
|
||||
#
|
||||
# Optional repository variable (gates the smoke-test job):
|
||||
# HAS_GPU_RUNNER = 'true' if a self-hosted GPU runner is available
|
||||
|
||||
name: Publish Blackwell Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
schedule:
|
||||
- cron: '17 4 * * 1' # weekly Mon 04:17 UTC (off-the-hour on purpose)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
unsloth_ref:
|
||||
description: 'unsloth git ref to bake in'
|
||||
required: false
|
||||
default: 'main'
|
||||
unsloth_zoo_ref:
|
||||
description: 'unsloth-zoo git ref to bake in'
|
||||
required: false
|
||||
default: 'main'
|
||||
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: unsloth/unsloth
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest # no GPU, 16GB RAM, 4 vCPU
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Free up ~20GB on the runner so cu128 wheels + cudnn fit.
|
||||
- name: Reclaim disk
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
|
||||
/opt/hostedtoolcache/CodeQL "$AGENT_TOOLSDIRECTORY"
|
||||
df -h /
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Resolve tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=ref,event=tag
|
||||
type=schedule,pattern=nightly
|
||||
type=sha,prefix=sha-,format=short
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
CUDA_VERSION=12.8.1
|
||||
UBUNTU_VERSION=24.04
|
||||
PYTHON_VERSION=3.12
|
||||
UNSLOTH_REF=${{ github.event.inputs.unsloth_ref || 'main' }}
|
||||
UNSLOTH_ZOO_REF=${{ github.event.inputs.unsloth_zoo_ref || 'main' }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo "${{ steps.meta.outputs.tags }} -> ${{ steps.meta.outputs.digest }}"
|
||||
|
||||
# Optional: pull the freshly published image onto a self-hosted GPU runner
|
||||
# and run smoke_test.py. Keeps "did the image actually work" decoupled from
|
||||
# "was a GPU available at build time". Skipped automatically when no GPU
|
||||
# runner is registered.
|
||||
smoke-test:
|
||||
needs: build
|
||||
if: ${{ vars.HAS_GPU_RUNNER == 'true' }}
|
||||
runs-on: [self-hosted, gpu]
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Pull and smoke-test
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker run --rm --gpus all \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||
python /workspace/smoke_test.py
|
||||
Loading…
Add table
Add a link
Reference in a new issue