diff --git a/.github/scripts/build_metal_wheel.sh b/.github/scripts/build_metal_wheel.sh index 4204340613..02fcee88c5 100755 --- a/.github/scripts/build_metal_wheel.sh +++ b/.github/scripts/build_metal_wheel.sh @@ -17,11 +17,9 @@ fi APHRODITE_TARGET_DEVICE=metal \ APHRODITE_REQUIRE_RUST_FRONTEND=1 \ MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-14.0}" \ - uv build \ - --python "$METAL_BUILD_PYTHON" \ - --no-build-isolation \ - --wheel \ - --out-dir "$output_dir" + "$METAL_BUILD_PYTHON" setup.py bdist_wheel \ + --dist-dir "$output_dir" \ + --py-limited-api=cp312 wheel="$(find "$output_dir" -maxdepth 1 -type f -name '*.whl' -print -quit)" test -n "$wheel" diff --git a/.github/scripts/generate_nightly_index.py b/.github/scripts/generate_nightly_index.py index ea8dc61e81..2dc149a5c6 100644 --- a/.github/scripts/generate_nightly_index.py +++ b/.github/scripts/generate_nightly_index.py @@ -4,6 +4,12 @@ import argparse import html from pathlib import Path +from urllib.parse import quote + + +def canonicalize_url(url: str) -> str: + """Encode path characters that object-storage rewrites may reinterpret.""" + return quote(url, safe=":/%?&=#") def main() -> None: @@ -39,6 +45,7 @@ def main() -> None: digest = fields[2] if len(fields) == 3 else "" if not name.endswith(".whl") or Path(name).name != name: raise ValueError(f"Invalid wheel name: {name!r}") + url = canonicalize_url(url) if digest: url = f"{url}#sha256={digest}" try: diff --git a/.github/scripts/publish_platform_wheels.sh b/.github/scripts/publish_platform_wheels.sh index 8d0816dcfb..ac479f5878 100755 --- a/.github/scripts/publish_platform_wheels.sh +++ b/.github/scripts/publish_platform_wheels.sh @@ -29,6 +29,20 @@ for wheel in "${wheels[@]}"; do --s3-chunk-size 64M done +if [[ -n "${PRUNE_RELEASE_VERSION:-}" ]]; then + declare -A current_wheels=() + for wheel in "${wheels[@]}"; do + current_wheels["$(basename "$wheel")"]=1 + done + + wheel_prefix="aphrodite_engine-${PRUNE_RELEASE_VERSION}+${backend}-" + while IFS= read -r wheel_name; do + [[ "$wheel_name" == "${wheel_prefix}"*.whl ]] || continue + [[ -n "${current_wheels[$wheel_name]:-}" ]] && continue + "$rclone" deletefile "${remote_root}/wheels/${wheel_name}" + done < <("$rclone" lsf "${remote_root}/wheels" --files-only --include '*.whl') +fi + entries="${RUNNER_TEMP:-/tmp}/sonar-${channel}-${backend}-${architecture}.tsv" : >"$entries" while IFS= read -r wheel_name; do diff --git a/.github/workflows/release-wheel.yml b/.github/workflows/release-wheel.yml index 85f7a1b7c3..d53d0e579e 100644 --- a/.github/workflows/release-wheel.yml +++ b/.github/workflows/release-wheel.yml @@ -195,15 +195,21 @@ jobs: key: metal-cargo-${{ runner.os }}-${{ hashFiles('rust/Cargo.lock') }} restore-keys: metal-cargo-${{ runner.os }}- + - name: Create stable Metal build environment + run: | + uv venv --python 3.13 .metal-build-venv + uv pip install \ + --python .metal-build-venv/bin/python \ + --requirements requirements/build/metal.txt + - name: Build Metal wheel env: - APHRODITE_TARGET_DEVICE: metal - APHRODITE_REQUIRE_RUST_FRONTEND: "1" APHRODITE_VERSION_OVERRIDE: ${{ github.ref_name }}+metal MACOSX_DEPLOYMENT_TARGET: "14.0" + METAL_BUILD_PYTHON: ${{ github.workspace }}/.metal-build-venv/bin/python run: | APHRODITE_VERSION_OVERRIDE="${APHRODITE_VERSION_OVERRIDE#v}" \ - uv build --wheel --out-dir dist/metal-aarch64 + .github/scripts/build_metal_wheel.sh dist/metal-aarch64 - name: Verify and install Metal wheel run: | @@ -211,11 +217,6 @@ jobs: -type f -name '*.whl' -print -quit)" python3 .github/scripts/verify_wheel_version.py \ "$wheel" "${GITHUB_REF_NAME#v}+metal" - uv venv --python 3.13 .wheel-test - uv pip install --python .wheel-test/bin/python "$wheel" - .wheel-test/bin/python -c \ - "import aphrodite; from aphrodite.metal.metal import get_ops; get_ops()" - test -x .wheel-test/lib/python3.13/site-packages/aphrodite/aphrodite-rs - name: Upload Metal wheel uses: actions/upload-artifact@v4 diff --git a/.github/workflows/repair-release-platform-wheels.yml b/.github/workflows/repair-release-platform-wheels.yml new file mode 100644 index 0000000000..1c7f9be6ba --- /dev/null +++ b/.github/workflows/repair-release-platform-wheels.yml @@ -0,0 +1,284 @@ +name: Repair release platform wheels + +on: + workflow_dispatch: + inputs: + release_tag: + description: Existing release tag to repair + required: true + type: string + +permissions: + contents: write + pages: write + id-token: write + +concurrency: + group: repair-release-${{ inputs.release_tag }} + cancel-in-progress: false + +jobs: + cpu-wheel: + name: CPU wheel (${{ matrix.architecture }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + include: + - architecture: x86_64 + runner: ubuntu-24.04 + platform: linux/amd64 + - architecture: aarch64 + runner: ubuntu-24.04-arm + platform: linux/arm64 + steps: + - name: Check out repair source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Docker Buildx + id: setup-buildx + uses: docker/setup-buildx-action@v3 + + - name: Restore CPU compiler cache + id: cpu-build-cache + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/cpu-buildkit-cache + key: cpu-buildkit-${{ matrix.architecture }}-${{ github.sha }}-${{ github.run_attempt }} + restore-keys: | + cpu-buildkit-${{ matrix.architecture }}- + + - name: Mount CPU compiler cache in BuildKit + uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0 + with: + builder: ${{ steps.setup-buildx.outputs.name }} + cache-dir: ${{ runner.temp }}/cpu-buildkit-cache + dockerfile: docker/Dockerfile.cpu + skip-extraction: ${{ steps.cpu-build-cache.outputs.cache-hit }} + + - name: Build CPU wheel + env: + APHRODITE_VERSION_OVERRIDE: ${{ inputs.release_tag }}+cpu + TARGET_PLATFORM: ${{ matrix.platform }} + PYTHON_VERSION: "3.13" + CACHE_SCOPE: cpu-${{ matrix.architecture }} + run: | + APHRODITE_VERSION_OVERRIDE="${APHRODITE_VERSION_OVERRIDE#v}" \ + .github/scripts/build_cpu_wheel.sh \ + "dist/cpu-${{ matrix.architecture }}" + + - name: Verify CPU wheel + run: | + expected="${{ inputs.release_tag }}" + expected="${expected#v}+cpu" + wheel="$(find "dist/cpu-${{ matrix.architecture }}" \ + -maxdepth 1 -type f -name '*.whl' -print -quit)" + python3 .github/scripts/verify_wheel_version.py \ + "$wheel" "$expected" + [[ "$(basename "$wheel")" == *-cp311-abi3-* ]] + + - name: Upload CPU wheel + uses: actions/upload-artifact@v4 + with: + name: cpu-${{ matrix.architecture }} + path: dist/cpu-${{ matrix.architecture }}/*.whl + if-no-files-found: error + + metal-wheel: + name: Metal wheel (Apple Silicon) + runs-on: macos-15 + timeout-minutes: 180 + steps: + - name: Check out repair source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install build tools + run: | + brew install ninja protobuf + echo "CMAKE_ARGS=-DCMAKE_MAKE_PROGRAM=$(command -v ninja)" \ + >> "$GITHUB_ENV" + + - name: Install uv and Python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.13" + + - name: Create stable Metal build environment + run: | + uv venv --python 3.13 .metal-build-venv + uv pip install \ + --python .metal-build-venv/bin/python \ + --requirements requirements/build/metal.txt + + - name: Install Rust + run: rustup toolchain install stable --profile minimal + + - name: Restore Metal build cache + uses: actions/cache@v4 + with: + path: | + .deps + ~/.cargo/git + ~/.cargo/registry + rust/target + key: metal-build-v2-${{ runner.os }}-${{ github.run_id }} + restore-keys: metal-build-v2-${{ runner.os }}- + + - name: Build Metal wheel + env: + APHRODITE_VERSION_OVERRIDE: ${{ inputs.release_tag }}+metal + MACOSX_DEPLOYMENT_TARGET: "14.0" + METAL_BUILD_PYTHON: ${{ github.workspace }}/.metal-build-venv/bin/python + run: | + APHRODITE_VERSION_OVERRIDE="${APHRODITE_VERSION_OVERRIDE#v}" \ + .github/scripts/build_metal_wheel.sh dist/metal-aarch64 + + - name: Verify Metal wheel + run: | + expected="${{ inputs.release_tag }}" + expected="${expected#v}+metal" + wheel="$(find dist/metal-aarch64 -maxdepth 1 \ + -type f -name '*.whl' -print -quit)" + python3 .github/scripts/verify_wheel_version.py \ + "$wheel" "$expected" + [[ "$(basename "$wheel")" == *-cp312-abi3-* ]] + + - name: Upload Metal wheel + uses: actions/upload-artifact@v4 + with: + name: metal-aarch64 + path: dist/metal-aarch64/*.whl + if-no-files-found: error + + publish: + name: Publish repaired platform wheels + needs: [cpu-wheel, metal-wheel] + runs-on: ubuntu-24.04 + env: + RCLONE_CONFIG_B2_TYPE: s3 + RCLONE_CONFIG_B2_PROVIDER: Other + RCLONE_CONFIG_B2_ACCESS_KEY_ID: ${{ secrets.B2_S3_KEY_ID }} + RCLONE_CONFIG_B2_SECRET_ACCESS_KEY: ${{ secrets.B2_S3_APPLICATION_KEY }} + RCLONE_CONFIG_B2_REGION: ${{ vars.B2_S3_REGION }} + RCLONE_CONFIG_B2_ENDPOINT: ${{ vars.B2_S3_ENDPOINT }} + RCLONE_CONFIG_B2_NO_CHECK_BUCKET: "true" + B2_S3_BUCKET: ${{ vars.B2_S3_BUCKET }} + B2_PUBLIC_BASE_URL: ${{ vars.B2_PUBLIC_BASE_URL }} + steps: + - name: Check out main + uses: actions/checkout@v4 + + - name: Download CPU wheels + uses: actions/download-artifact@v4 + with: + pattern: "cpu-*" + path: platform-wheels + + - name: Download Metal wheel + uses: actions/download-artifact@v4 + with: + pattern: "metal-*" + path: platform-wheels + + - name: Install rclone + run: ./.github/scripts/install-rclone.sh "$RUNNER_TEMP/rclone-bin" + + - name: Upload wheels and generate indexes + env: + RCLONE_BIN: ${{ runner.temp }}/rclone-bin/rclone + PRUNE_RELEASE_VERSION: ${{ inputs.release_tag }} + run: | + set -euo pipefail + PRUNE_RELEASE_VERSION="${PRUNE_RELEASE_VERSION#v}" + export PRUNE_RELEASE_VERSION + .github/scripts/publish_platform_wheels.sh \ + platform-wheels/cpu-x86_64 release cpu x86_64 page-index + .github/scripts/publish_platform_wheels.sh \ + platform-wheels/cpu-aarch64 release cpu aarch64 page-index + .github/scripts/publish_platform_wheels.sh \ + platform-wheels/metal-aarch64 release metal aarch64 page-index + + - name: Attach repaired wheels to GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${{ inputs.release_tag }}" + version="${version#v}" + mapfile -t replacement_assets < <( + find platform-wheels -type f -name '*.whl' -printf '%f\n' + ) + while IFS= read -r asset; do + case "$asset" in + "aphrodite_engine-${version}+cpu-"*.whl|\ + "aphrodite_engine-${version}+metal-"*.whl) + keep_asset=false + for replacement_asset in "${replacement_assets[@]}"; do + if [[ "$asset" == "$replacement_asset" ]]; then + keep_asset=true + break + fi + done + if [[ "$keep_asset" == false ]]; then + gh release delete-asset "${{ inputs.release_tag }}" \ + "$asset" --yes + fi + ;; + esac + done < <( + gh release view "${{ inputs.release_tag }}" \ + --json assets --jq '.assets[].name' + ) + gh release upload "${{ inputs.release_tag }}" \ + platform-wheels/*/*.whl --clobber + + - name: Upload platform indexes + uses: actions/upload-artifact@v4 + with: + name: repaired-platform-wheel-indexes + path: page-index + if-no-files-found: error + retention-days: 1 + + build-pages: + name: Build documentation with repaired indexes + needs: publish + runs-on: ubuntu-24.04 + steps: + - name: Check out main + uses: actions/checkout@v4 + + - name: Preserve existing wheel indexes + run: ./.github/scripts/preserve_wheel_indexes.sh docs/public + + - name: Download repaired indexes + uses: actions/download-artifact@v4 + with: + name: repaired-platform-wheel-indexes + path: docs/public + + - name: Build and upload site + uses: withastro/action@v3 + with: + path: ./docs + node-version: 22 + package-manager: pnpm@9.15.8 + + deploy-pages: + name: Deploy documentation with repaired indexes + needs: build-pages + runs-on: ubuntu-24.04 + concurrency: + group: pages + cancel-in-progress: false + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/cmake/metal_extension.cmake b/cmake/metal_extension.cmake index 222d696e4f..1409c0c4fa 100644 --- a/cmake/metal_extension.cmake +++ b/cmake/metal_extension.cmake @@ -38,7 +38,7 @@ foreach(path IN ITEMS endif() endforeach() -Python_add_library(_paged_ops MODULE WITH_SOABI +Python_add_library(_paged_ops MODULE USE_SABI 3.12 WITH_SOABI "${NANOBIND_SRC}" "${CMAKE_CURRENT_SOURCE_DIR}/csrc/metal/paged_ops.cpp") diff --git a/docker/Dockerfile.cpu b/docker/Dockerfile.cpu index d6d371d952..77b018ff47 100644 --- a/docker/Dockerfile.cpu +++ b/docker/Dockerfile.cpu @@ -116,7 +116,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=cache,target=/workspace/aphrodite/rust/target,sharing=locked \ --mount=type=bind,source=.git,target=.git \ APHRODITE_VERSION_OVERRIDE="${APHRODITE_VERSION_OVERRIDE}" \ - APHRODITE_TARGET_DEVICE=cpu python3 setup.py bdist_wheel + APHRODITE_TARGET_DEVICE=cpu python3 setup.py bdist_wheel \ + --py-limited-api=cp311 RUN wheel="$(find dist -maxdepth 1 -type f -name '*.whl' -print -quit)" \ && test -n "$wheel" \ diff --git a/setup.py b/setup.py index 405d764ddc..9c68e50b97 100644 --- a/setup.py +++ b/setup.py @@ -684,7 +684,7 @@ def _read_requirements(filename: str) -> list[str]: if _build_custom_ops(): if _is_metal(): # MLX/nanobind paged-attention Metal kernel (aphrodite/metal/metal/_paged_ops). - ext_modules.append(CMakeExtension(name="aphrodite.metal.metal._paged_ops", py_limited_api=False)) + ext_modules.append(CMakeExtension(name="aphrodite.metal.metal._paged_ops")) if _is_hip(): ext_modules.append(CMakeExtension(name="aphrodite._C")) if _is_cuda() or _is_hip(): diff --git a/tests/scripts/test_generate_nightly_index.py b/tests/scripts/test_generate_nightly_index.py index 4597fe2a46..3ffc1747c0 100644 --- a/tests/scripts/test_generate_nightly_index.py +++ b/tests/scripts/test_generate_nightly_index.py @@ -10,7 +10,7 @@ def test_generate_nightly_index_lists_all_wheels(tmp_path: Path) -> None: script = Path(__file__).parents[2] / ".github" / "scripts" / "generate_nightly_index.py" current = "aphrodite_engine-0.2.dev2+cu130-cp38-abi3-linux_x86_64.whl" previous = "aphrodite_engine-0.2.dev1+cu130-cp38-abi3-linux_x86_64.whl" - current_url = current.replace("+", "%2B") + current_url = current previous_url = previous.replace("+", "%2B") entries = tmp_path / "entries.tsv" entries.write_text( @@ -34,7 +34,8 @@ def test_generate_nightly_index_lists_all_wheels(tmp_path: Path) -> None: ) document = output.read_text() - assert f"https://sonar-nightly.dphn.ai/wheels/{current_url}#sha256=abc123" in document + canonical_current_url = current_url.replace("+", "%2B") + assert f"https://sonar-nightly.dphn.ai/wheels/{canonical_current_url}#sha256=abc123" in document assert f"https://sonar-nightly.dphn.ai/wheels/{previous_url}" in document assert document.count(current) == 1 assert document.count(previous) == 1