diff --git a/.github/workflows/build-vllm.yml b/.github/workflows/build-vllm.yml new file mode 100644 index 00000000000..70971056cab --- /dev/null +++ b/.github/workflows/build-vllm.yml @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.cpu +name: Build vllm wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/vllm.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-vllm.yml' + - 'docs/packages/vllm.yaml' + - 'patches/vllm/**' + push: + branches: [main] + paths: + - '.github/workflows/build-vllm.yml' + - 'docs/packages/vllm.yaml' + - 'patches/vllm/**' + +run-name: build-vllm ${{ inputs.version && format('- {0}', inputs.version) || '' }} + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: vllm + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build vllm ${{ matrix.version }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + + env: + VLLM_VERSION: ${{ matrix.version }} + + steps: + # The wheel carries vLLM's CPU-build local segment (0.29.0+cpu); the tag does not. + - id: ref + run: echo "ref=v${VLLM_VERSION%+cpu}" >> "$GITHUB_OUTPUT" + + - name: Checkout vllm ${{ steps.ref.outputs.ref }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: vllm-project/vllm + ref: ${{ steps.ref.outputs.ref }} + path: vllm + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch vllm source + working-directory: vllm + run: | + git apply ../python-wheels/patches/vllm/${{ env.VLLM_VERSION }}/*.patch + + - uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: vllm + output-dir: wheelhouse/ + env: + CIBW_BUILD: "cp312-manylinux_riscv64" + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # csrc/cpu/utils.cpp #includes unconditionally on the CPU backend; + # the manylinux image doesn't ship it. + CIBW_BEFORE_ALL_LINUX: >- + dnf install -y --setopt=install_weak_deps=False numactl-devel + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs + | sh -s -- -y --default-toolchain none + CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation" + # Upstream's Dockerfile.cpu passes --py-limited-api to bdist_wheel; cibuildwheel + # does not, and without it the abi3 extensions get a cp312-only wheel tag. cp312 + # rather than upstream's cp38 because torch 2.13.0 starts at cp312 on riscv64, so + # cp312 is the oldest interpreter this wheel is ever built on (gotcha 96). + CIBW_CONFIG_SETTINGS: "--build-option=--py-limited-api=cp312" + CIBW_BEFORE_BUILD: >- + pip install --only-binary=:all: -r {package}/requirements/build/cpu.txt + CIBW_ENVIRONMENT: >- + VLLM_TARGET_DEVICE=cpu + VLLM_VERSION_OVERRIDE=${{ env.VLLM_VERSION }} + CMAKE_ARGS=-DVLLM_RVV_VLEN=0 + PATH="$PATH:$HOME/.cargo/bin" + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + # The torch wheel already carries these and loads them RTLD_GLOBAL. + CIBW_REPAIR_WHEEL_COMMAND: >- + auditwheel repair -w {dest_dir} {wheel} + --exclude libtorch.so + --exclude libtorch_cpu.so + --exclude libtorch_python.so + --exclude libtorch_global_deps.so + --exclude libc10.so + --exclude libgomp.so.1 + CIBW_TEST_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + VLLM_TARGET_DEVICE=cpu + CIBW_TEST_COMMAND: >- + python -c "import vllm, vllm._C, torch; + from vllm.platforms import current_platform; + print(vllm.__version__, current_platform.get_cpu_architecture())" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: vllm-${{ env.VLLM_VERSION }}-abi3-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish vllm ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: vllm-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/vllm.yaml b/docs/packages/vllm.yaml new file mode 100644 index 00000000000..7146d60efe4 --- /dev/null +++ b/docs/packages/vllm.yaml @@ -0,0 +1,5 @@ +package-name: vllm +source-code: https://github.com/vllm-project/vllm +license: Apache-2.0 +versions: +- version: 0.29.0+cpu diff --git a/patches/vllm/0.29.0+cpu/0001-requirements-do-not-require-numba-on-riscv64.patch b/patches/vllm/0.29.0+cpu/0001-requirements-do-not-require-numba-on-riscv64.patch new file mode 100644 index 00000000000..a51b47a73e3 --- /dev/null +++ b/patches/vllm/0.29.0+cpu/0001-requirements-do-not-require-numba-on-riscv64.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000001 Mon Sep 17 00:00:00 2001 +From: RISE Project CI +Date: Sat, 19 Sep 2026 00:00:00 +0000 +Subject: [PATCH] [CPU] Do not require numba on riscv64 + +requirements/cpu.txt requires numba on every CPU architecture except +s390x. numba has no riscv64 wheel on any index, and it cannot be built +from its sdist either: it hard-depends on llvmlite, whose build links a +patched LLVM distributed only as a conda package on a channel that +publishes no linux-riscv64 subdir. An unpatched riscv64 CPU wheel is +therefore buildable but not installable. + +numba is optional to vLLM by construction - vllm/utils/import_utils.py +declares is_numba_available() "Whether the optional `numba` package is +available" and the Kimi-K2.5 fused vision processor falls back to the +remote HF processor when it is absent. Excluding it costs riscv64 the +same features s390x already goes without (n-gram speculative decoding +and the fused vision/inkling processors), which is exactly the +configuration upstream already ships for s390x. + +Upstream-Status: To upstream [vLLM publishes no riscv64 wheels and runs no riscv64 CI, so there is no upstream build this marker would affect yet; resubmit once riscv64 joins docs/getting_started/installation/cpu.md and the CPU release matrix] + +Signed-off-by: RISE Project CI +--- + requirements/cpu.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/requirements/cpu.txt b/requirements/cpu.txt +index 30e47a8..e4ce77b 100644 +--- a/requirements/cpu.txt ++++ b/requirements/cpu.txt +@@ -3,7 +3,7 @@ + + setuptools==77.0.3 # this version can reuse CMake build dir + +-numba == 0.65.0; platform_machine != "s390x" # Required for N-gram speculative decoding ++numba == 0.65.0; platform_machine != "s390x" and platform_machine != "riscv64" # Required for N-gram speculative decoding + + # Dependencies for CPUs + torch==2.13.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64" +-- +2.51.0 diff --git a/patches/vllm/0.29.0+cpu/0002-cpu-probe-the-torch-openblas-for-sbgemm.patch b/patches/vllm/0.29.0+cpu/0002-cpu-probe-the-torch-openblas-for-sbgemm.patch new file mode 100644 index 00000000000..33bed09ffa2 --- /dev/null +++ b/patches/vllm/0.29.0+cpu/0002-cpu-probe-the-torch-openblas-for-sbgemm.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000002 Mon Sep 17 00:00:00 2001 +From: RISE Project CI +Date: Sun, 20 Sep 2026 00:00:00 +0000 +Subject: [PATCH] [CPU] Probe the torch wheel's OpenBLAS for sbgemm_ + +cpu_extension.cmake decides whether to compile the OpenBLAS bf16 GEMM path +by globbing the installed torch wheel for libopenblas*.so*, and defines +VLLM_HAS_OPENBLAS when the glob hits. Presence of the library is not the +same thing as presence of the symbol: blas_gemm() in +csrc/cpu/sgl-kernels/blas_gemm.h calls sbgemm_, and OpenBLAS only builds +sbgemm_ for the architectures it has BFLOAT16 kernels for (x86_64 +Cooper Lake and later, ARM64 Neoverse, POWER10, z14 and later). RISC-V is +not one of them. + +The miss cannot be caught at link time, because the comment right above +the glob is accurate - _C deliberately does not link OpenBLAS and expects +libtorch.so to have loaded it RTLD_GLOBAL - so a shared object with an +unresolved sbgemm_ links happily and the build succeeds. It fails on the +first "import vllm._C" instead: + + ImportError: .../vllm/_C.abi3.so: undefined symbol: sbgemm_ + +blas_gemm.h already carries the right fallback for this case: its #else +branch dispatches through at::native::cpublas::gemm_no_downcast_stub, +which libtorch_cpu.so exports on every architecture. So ask the library +whether it actually provides sbgemm_ and take that fallback when it does +not, rather than assuming every non-x86 torch wheel ships a bf16-capable +OpenBLAS. When the probe itself cannot run the previous behaviour is kept, +so no platform that works today changes. + +Upstream-Status: To upstream [Architecture-agnostic: the probe is a no-op wherever OpenBLAS does export sbgemm_, and it fixes any platform whose torch wheel ships an OpenBLAS built without BUILD_BFLOAT16, riscv64 being the one this repo builds for] + +Signed-off-by: RISE Project CI +--- + cmake/cpu_extension.cmake | 20 ++++++++++++++++++-- + 1 file changed, 18 insertions(+), 2 deletions(-) + +diff --git a/cmake/cpu_extension.cmake b/cmake/cpu_extension.cmake +index 92c3daa..a7a229c 100644 +--- a/cmake/cpu_extension.cmake ++++ b/cmake/cpu_extension.cmake +@@ -430,8 +430,24 @@ if (NOT ENABLE_X86_ISA) + "${TORCH_INSTALL_PREFIX}/lib/libopenblas*.so*") + # Note: we don't link openblas directly to _C extension, as it's available through libtorch.so + if (_VLLM_TORCH_OPENBLAS_LIBS) +- list(GET _VLLM_TORCH_OPENBLAS_LIBS 0 VLLM_OPENBLAS_LIB) +- message(STATUS "CPU OpenBLAS library: ${VLLM_OPENBLAS_LIB}") ++ list(GET _VLLM_TORCH_OPENBLAS_LIBS 0 _VLLM_TORCH_OPENBLAS_LIB) ++ # blas_gemm() calls OpenBLAS' bf16 GEMM, which OpenBLAS only builds for ++ # the architectures it has BFLOAT16 kernels for. Elsewhere the library is ++ # present but sbgemm_ is not, and since _C resolves it through ++ # libtorch.so instead of linking OpenBLAS itself, the miss is not ++ # reported until the extension is imported. Probe for the symbol and keep ++ # the PyTorch reference BLAS path when it is absent. ++ execute_process( ++ COMMAND ${CMAKE_NM} --dynamic --defined-only "${_VLLM_TORCH_OPENBLAS_LIB}" ++ OUTPUT_VARIABLE _VLLM_TORCH_OPENBLAS_SYMBOLS ++ ERROR_QUIET) ++ if (_VLLM_TORCH_OPENBLAS_SYMBOLS STREQUAL "" OR ++ _VLLM_TORCH_OPENBLAS_SYMBOLS MATCHES "[ \t]sbgemm_[\r\n]") ++ set(VLLM_OPENBLAS_LIB "${_VLLM_TORCH_OPENBLAS_LIB}") ++ message(STATUS "CPU OpenBLAS library: ${VLLM_OPENBLAS_LIB}") ++ else() ++ message(STATUS "CPU OpenBLAS library ${_VLLM_TORCH_OPENBLAS_LIB} provides no sbgemm_, using the PyTorch reference BLAS path") ++ endif() + endif() + endif() + +-- +2.51.0