From baf349eff94c39fe6e28d64f232f609f1b4dc87b Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Mon, 16 Feb 2026 08:52:31 -0500 Subject: [PATCH 1/3] chore: bump to llmd 0.5.0 - Increase dev mode GCE disk to 300GB pd-balanced for compilation - Keep production mode at 100GB pd-ssd for optimal serving performance - Fixes exit code 128 failures due to insufficient disk space during Rust compilation Signed-off-by: Nick Mitchell --- .github/workflows/test-llm-d-patch.yml | 7 +- .github/workflows/vllm-gce.yml | 4 +- cli/src/args.rs | 2 +- cli/src/main.rs | 2 +- docker/gce/vllm/cloud-config.yaml | 2 +- docker/gce/vllm/create-vllm-gce-image.sh | 2 +- docker/gce/vllm/setup-dev.sh | 4 +- docker/gce/vllm/setup.sh | 207 ------------------ docker/vllm/llm-d/clone.sh | 10 +- docker/vllm/llm-d/genpatch.sh | 23 +- .../patches/0.5.0/01-spans-llmd-vllm.patch.gz | Bin 0 -> 10847 bytes spnl/src/vllm/gce/args.rs | 10 +- spnl/src/vllm/gce/image.rs | 50 +++-- spnl/src/vllm/gce/up.rs | 31 ++- spnl/src/vllm/patch.rs | 2 +- 15 files changed, 86 insertions(+), 270 deletions(-) delete mode 100644 docker/gce/vllm/setup.sh create mode 100644 docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz diff --git a/.github/workflows/test-llm-d-patch.yml b/.github/workflows/test-llm-d-patch.yml index 01170d9a..b730c35f 100644 --- a/.github/workflows/test-llm-d-patch.yml +++ b/.github/workflows/test-llm-d-patch.yml @@ -31,9 +31,10 @@ jobs: git config --global user.email "foo@bar.com" git config --global user.name "Spnl Dev" - - name: Generate patch - working-directory: docker/vllm/llm-d - run: ./genpatch.sh + # llmd 0.5.0 required AI help to rebase... We can re-enable this if we ever rebase our vllm span query branch to avoid the conflicts that come up when running genpatch + #- name: Generate patch + # working-directory: docker/vllm/llm-d + # run: ./genpatch.sh - name: Apply patch working-directory: docker/vllm/llm-d diff --git a/.github/workflows/vllm-gce.yml b/.github/workflows/vllm-gce.yml index 280ec115..4f3e87b7 100644 --- a/.github/workflows/vllm-gce.yml +++ b/.github/workflows/vllm-gce.yml @@ -20,9 +20,9 @@ jobs: name: Test in GCE VM env: # Adjust these as needed - VLLM_ORG: neuralmagic + VLLM_ORG: vllm-project VLLM_REPO: vllm - VLLM_BRANCH: llm-d-release-0.4 + VLLM_SHA: d7de043d55d1dd629554467e23874097e1c48993 MODEL: ibm-granite/granite-3.3-2b-instruct # You probably won't need to change this diff --git a/cli/src/args.rs b/cli/src/args.rs index 5648b55a..69b23fb7 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -226,7 +226,7 @@ pub enum ImageCommands { image_family: String, /// LLM-D version for patch file - #[arg(long, default_value = "0.4.0")] + #[arg(long, default_value = "0.5.0")] llmd_version: String, /// GCE configuration diff --git a/cli/src/main.rs b/cli/src/main.rs index c2a0da6c..4c154cd7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -114,7 +114,7 @@ async fn main() -> Result<(), SpnlError> { .llmd_version(llmd_version.clone()) .vllm_org(gce_config.vllm_org.clone()) .vllm_repo(gce_config.vllm_repo.clone()) - .vllm_branch(gce_config.vllm_branch.clone()) + .vllm_sha(gce_config.vllm_sha.clone()) .config(gce_config.clone()) .build()?, ) diff --git a/docker/gce/vllm/cloud-config.yaml b/docker/gce/vllm/cloud-config.yaml index fe1db798..4b9eb474 100644 --- a/docker/gce/vllm/cloud-config.yaml +++ b/docker/gce/vllm/cloud-config.yaml @@ -23,7 +23,7 @@ write_files: SPNL_RELEASE=${spnl_release} VLLM_ORG=${vllm_org} VLLM_REPO=${vllm_repo} - VLLM_BRANCH=${vllm_branch} + VLLM_SHA=${vllm_sha} MODEL=${model} VLLM_PATCHFILE=/tmp/vllm.patch ${vllm_config_section} diff --git a/docker/gce/vllm/create-vllm-gce-image.sh b/docker/gce/vllm/create-vllm-gce-image.sh index c332cfe2..8da56de4 100755 --- a/docker/gce/vllm/create-vllm-gce-image.sh +++ b/docker/gce/vllm/create-vllm-gce-image.sh @@ -2,7 +2,7 @@ # # Create a custom GCE image with vLLM pre-installed -# This script creates a reusable image based on the setup.sh logic +# This script creates a reusable image # set -euo pipefail diff --git a/docker/gce/vllm/setup-dev.sh b/docker/gce/vllm/setup-dev.sh index 8410317b..1298bcda 100644 --- a/docker/gce/vllm/setup-dev.sh +++ b/docker/gce/vllm/setup-dev.sh @@ -69,8 +69,10 @@ fi # Install vLLM curl -LsSf https://astral.sh/uv/install.sh | sh source $HOME/.local/bin/env -git clone https://github.com/$VLLM_ORG/$VLLM_REPO.git vllm -b $VLLM_BRANCH +git clone https://github.com/$VLLM_ORG/$VLLM_REPO.git vllm cd vllm +git fetch origin $VLLM_SHA +git checkout $VLLM_SHA uv venv --seed source .venv/bin/activate VLLM_USE_PRECOMPILED=1 uv pip install --editable . diff --git a/docker/gce/vllm/setup.sh b/docker/gce/vllm/setup.sh deleted file mode 100644 index 6ce3a520..00000000 --- a/docker/gce/vllm/setup.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env bash - -# -# Note: this script is executed inside the VM, via a cloud-init runcmd. See ./cloud-config.yaml -# - -set -eo pipefail - -# DEBUG -#set -x - -cleanup() { - rc=$? - echo "Exiting with exit_code=$rc" - gsutil cp <(echo $rc) gs://$GCS_BUCKET/runs/$RUN_ID/status/exit_code -} -trap "cleanup" EXIT - -export HOME=/root -cd $HOME - -# TODO: i was expecting this to be loaded automatically. Apparently not if this is run via a cloud-init runcmd. -. /etc/environment - -export SCCACHE_GCS_BUCKET=$GCS_BUCKET -SCCACHE_VERSION=$(curl -s "https://api.github.com/repos/mozilla/sccache/releases/latest" | grep -Po '"tag_name": "v\K[0-9.]+') -wget -qO sccache.tar.gz https://github.com/mozilla/sccache/releases/latest/download/sccache-v$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz -mkdir sccache-temp -tar xf sccache.tar.gz --strip-components=1 -C sccache-temp -sudo mv sccache-temp/sccache /usr/local/bin -sudo chmod a+x /usr/local/bin/sccache -rm -rf sccache.tar.gz sccache-temp -export RUSTC_WRAPPER=/usr/local/bin/sccache -export SCCACHE_GCS_RW_MODE=READ_WRITE -export SCCACHE_GCS_KEY_PREFIX=sccache - -# Install and build spnl -export CARGO_INCREMENTAL=0 # Disable incremental compilation for faster from-scratch builds -export CARGO_PROFILE_TEST_DEBUG=0 - -if [[ -n "$SPNL_RELEASE" ]] -then - echo "Downloading spnl release $SPNL_RELEASE" - - # Detect OS and architecture - OS=$(uname -s | tr '[:upper:]' '[:lower:]') - ARCH=$(uname -m) - - # Map architecture names to match GitHub release naming - case "$ARCH" in - x86_64) - ARCH="x86_64" - ;; - aarch64|arm64) - ARCH="aarch64" - ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; - esac - - # Map OS and ABI to match GitHub release naming - # Format: spnl-{version}-{os}-{arch}-{abi}.tar.gz - case "$OS" in - linux) - OS="linux" - ABI="gnu" - ;; - darwin) - OS="macos" - ABI="" - ;; - *) - echo "Unsupported OS: $OS" - exit 1 - ;; - esac - - # Construct the asset name - if [[ -n "$ABI" ]]; then - ASSET_NAME="spnl-${SPNL_RELEASE}-${OS}-${ARCH}-${ABI}.tar.gz" - else - ASSET_NAME="spnl-${SPNL_RELEASE}-${OS}-${ARCH}.tar.gz" - fi - - # Extract repo owner and name from SPNL_GITHUB (e.g., https://github.com/owner/repo) - REPO_PATH=$(echo "$SPNL_GITHUB" | sed -E 's|https?://github.com/||' | sed 's|\.git$||') - - # Download the release asset - DOWNLOAD_URL="https://github.com/${REPO_PATH}/releases/download/${SPNL_RELEASE}/${ASSET_NAME}" - echo "Downloading from: $DOWNLOAD_URL" - - wget -q "$DOWNLOAD_URL" -O spnl-release.tar.gz || { - echo "Failed to download release asset. Falling back to building from source." - exit 1 - } - - # Extract and install - tar xzf spnl-release.tar.gz - sudo cp spnl /usr/local/bin/ - sudo chmod a+rX /usr/local/bin/spnl - rm spnl-release.tar.gz spnl - - # No need to clone repo or build - we'll install Python package from PyPI later - spnl_pid=0 -elif [[ -n "$GITHUB_SHA" ]] && [[ -n "$GITHUB_REF" ]] -then - echo "Cloning spnl from GITHUB_SHA=$GITHUB_SHA GITHUB_REF=$GITHUB_REF" - ( - curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ - source $HOME/.cargo/env && \ - mkdir spnl && \ - cd spnl && \ - git init && \ - git remote add origin $SPNL_GITHUB && \ - git fetch --prune --no-recurse-submodules --depth=1 origin +$GITHUB_SHA:$GITHUB_REF && \ - git checkout --progress --force $GITHUB_REF && \ - cargo build -F rag,spnl-api,vllm && sudo cp target/debug/spnl /usr/local/bin && sudo chmod a+rX /usr/local/bin/spnl \ - ) & - spnl_pid=$! -else - echo "Cloning spnl from repo=$SPNL_GITHUB" - ( - curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ - source $HOME/.cargo/env && \ - git clone $SPNL_GITHUB spnl && \ - cd spnl && \ - cargo build -F rag,spnl-api,vllm && sudo cp target/debug/spnl /usr/local/bin && sudo chmod a+rX /usr/local/bin/spnl \ - ) & - spnl_pid=$! -fi - -# Install vLLM -curl -LsSf https://astral.sh/uv/install.sh | sh -source $HOME/.local/bin/env -git clone https://github.com/$VLLM_ORG/$VLLM_REPO.git vllm -b $VLLM_BRANCH -cd vllm -uv venv --seed -source .venv/bin/activate -VLLM_USE_PRECOMPILED=1 uv pip install --editable . - -# Wait for spnl build to complete (if building from source) -if [[ $spnl_pid -ne 0 ]]; then - echo "Waiting for spnl build to complete..." - wait $spnl_pid - echo "spnl build completed" -fi - -# Patch the vllm code and install spnl Python package -spnl vllm patchfile | git apply - -if [[ -n "$SPNL_RELEASE" ]] -then - # Install spnl from PyPI (strip 'v' prefix if present) - SPNL_VERSION="${SPNL_RELEASE#v}" - echo "Installing spnl==$SPNL_VERSION from PyPI" - uv pip install "spnl==$SPNL_VERSION" -else - # Build the cloned version of spnl into vLLM, via maturin - uv pip install maturin[patchelf] - source $HOME/.cargo/env # to get rustc on path - (cd $HOME/spnl && maturin develop -F tok,run_py -m spnl/Cargo.toml) -fi - -# Start vLLM -VLLM_ATTENTION_BACKEND=TRITON_ATTN \ - VLLM_USE_V1=1 \ - VLLM_V1_SPANS_ENABLED=True \ - VLLM_V1_SPANS_TOKEN_PLUS=10 \ - VLLM_V1_SPANS_TOKEN_CROSS=13 \ - VLLM_SERVER_DEV_MODE=1 \ - nohup vllm serve $MODEL --enforce-eager & - -# Install ollama (for embedding) -(curl -fsSL https://ollama.com/install.sh | sh && ollama serve) & - -# Wait till vllm is ready -timeout 5m bash -c 'until curl --output /dev/null --silent --fail http://localhost:8000/health; do sleep 3; done' -echo "vllm is ready" - -# Wait till ollama is ready -#timeout 5m bash -c 'until curl --output /dev/null --silent --fail http://localhost:11434; do sleep 3; done' -timeout 5m bash -c 'until ollama ps; do sleep 3; done' -echo "ollama is ready" - -# Run tests only if not using a release (releases are for production, not testing) -if [[ -z "$SPNL_RELEASE" ]] -then - # Here are the variables we will allow to be used in the test.d/* scripts - declare -x GCS_BUCKET - declare -x RUN_ID - declare -x MODEL - declare -x OPENAI_API_BASE=http://localhost:8000/v1 - - cd $HOME - TESTS_DIR=$HOME/spnl/docker/gce/vllm/test.d - if [ -d "$TESTS_DIR" ] - then - n_tests=$(ls "$TESTS_DIR" | wc -l | xargs) - echo "Starting $n_tests tests" - find "$TESTS_DIR" -type f -name '*.sh' -print0 | xargs -0L1 -I{} bash -c 'echo "Executing {} at $(date -u)"; "{}"' - else echo "No tests found in $TESTS_DIR" - fi -else - echo "Skipping tests (SPNL_RELEASE is set)" -fi diff --git a/docker/vllm/llm-d/clone.sh b/docker/vllm/llm-d/clone.sh index 4625c864..9bc22aa9 100755 --- a/docker/vllm/llm-d/clone.sh +++ b/docker/vllm/llm-d/clone.sh @@ -2,12 +2,14 @@ set -e -LLMD_VERSION=0.4.0 -BASE_VLLM_FORK=https://github.com/neuralmagic/vllm.git -BASE_VLLM_BRANCH=llm-d-release-0.4 +LLMD_VERSION=0.5.0 +BASE_VLLM_FORK=https://github.com/vllm-project/vllm.git +BASE_VLLM_COMMIT_SHA=d7de043d55d1dd629554467e23874097e1c48993 -git clone $BASE_VLLM_FORK -b $BASE_VLLM_BRANCH --depth 1 +git clone $BASE_VLLM_FORK vllm cd vllm +git fetch --depth=1 origin $BASE_VLLM_COMMIT_SHA +git checkout -q $BASE_VLLM_COMMIT_SHA for patchfile in ../patches/$LLMD_VERSION/*.patch.gz do git apply <(gunzip -c $patchfile) --reject diff --git a/docker/vllm/llm-d/genpatch.sh b/docker/vllm/llm-d/genpatch.sh index 74d4449d..ff9918e9 100755 --- a/docker/vllm/llm-d/genpatch.sh +++ b/docker/vllm/llm-d/genpatch.sh @@ -7,25 +7,24 @@ SCRIPTDIR=$(cd $(dirname "$0") && pwd) SPANS_VLLM_FORK=https://github.com/starpit/vllm-ibm.git SPANS_VLLM_BRANCH=spnl-ibm -LLMD_VERSION=0.4.0 -BASE_VLLM_FORK=https://github.com/neuralmagic/vllm.git -BASE_VLLM_BRANCH=llm-d-release-0.4 +LLMD_VERSION=0.5.0 +BASE_VLLM_FORK=https://github.com/vllm-project/vllm.git +BASE_VLLM_COMMIT_SHA=d7de043d55d1dd629554467e23874097e1c48993 -T=$(mktemp -d) -trap "rm -rf $T" EXIT +T=vllm +#trap "rm -rf $T" EXIT -git clone $BASE_VLLM_FORK $T/vllm-llmd -b $BASE_VLLM_BRANCH +git clone $BASE_VLLM_FORK $T/vllm-llmd cd $T/vllm-llmd -BASE_VLLM_REVISION=$(git rev-parse --verify HEAD) +git fetch origin $BASE_VLLM_COMMIT_SHA +git checkout -q $BASE_VLLM_COMMIT_SHA +BASE_VLLM_REVISION=$BASE_VLLM_COMMIT_SHA git remote add spans $SPANS_VLLM_FORK git fetch spans $SPANS_VLLM_BRANCH -git checkout $SPANS_VLLM_BRANCH -SPANS_VLLM_REVISION=$(git rev-parse --verify HEAD) -git checkout $BASE_VLLM_BRANCH -git rebase spans/$SPANS_VLLM_BRANCH -C0 +git rebase spans/$SPANS_VLLM_BRANCH -C0 # Notes: gzip --no-name ensures deterministic output (gzip won't save mtime in the file); this helps with git sanity -mkdir -p "$SCRIPTDIR"/patches +mkdir -p "$SCRIPTDIR"/patches/$LLMD_VERSION git diff $BASE_VLLM_REVISION | gzip --no-name -c > "$SCRIPTDIR"/patches/$LLMD_VERSION/01-spans-llmd-vllm.patch.gz diff --git a/docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz b/docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz new file mode 100644 index 0000000000000000000000000000000000000000..745d72dd45c73427bd03c39bc7a5c09a75e9796e GIT binary patch literal 10847 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 literal 0 HcmV?d00001 diff --git a/spnl/src/vllm/gce/args.rs b/spnl/src/vllm/gce/args.rs index 1d5ffcda..bf60dc5b 100644 --- a/spnl/src/vllm/gce/args.rs +++ b/spnl/src/vllm/gce/args.rs @@ -52,9 +52,9 @@ pub struct GceConfig { #[arg(long, env = "VLLM_REPO", default_value = "vllm")] pub vllm_repo: String, - /// vLLM branch to use - #[arg(long, env = "VLLM_BRANCH", default_value = "llm-d-release-0.4")] - pub vllm_branch: String, + /// vLLM commit SHA to use + #[arg(long, env = "VLLM_SHA", default_value = "a1b2c3d4e5f6")] + pub vllm_sha: String, } impl GceConfig { @@ -72,7 +72,7 @@ impl GceConfig { github_ref: None, vllm_org: "neuralmagic".to_string(), vllm_repo: "vllm".to_string(), - vllm_branch: "llm-d-release-0.4".to_string(), + vllm_sha: "a1b2c3d4e5f6".to_string(), } } @@ -126,7 +126,7 @@ mod tests { assert_eq!(config.spnl_github, None); assert_eq!(config.vllm_org, "neuralmagic"); assert_eq!(config.vllm_repo, "vllm"); - assert_eq!(config.vllm_branch, "llm-d-release-0.4"); + assert_eq!(config.vllm_sha, "a1b2c3d4e5f6"); } #[test] diff --git a/spnl/src/vllm/gce/image.rs b/spnl/src/vllm/gce/image.rs index 8756ed01..d9a0f947 100644 --- a/spnl/src/vllm/gce/image.rs +++ b/spnl/src/vllm/gce/image.rs @@ -1,11 +1,11 @@ use super::args::GceConfig; /// Generate image name from patch content hash and vLLM source identifier -fn generate_image_name( +pub fn generate_image_name( patch_content: &[u8], vllm_org: &str, vllm_repo: &str, - vllm_branch: &str, + vllm_sha: &str, ) -> String { use sha2::{Digest, Sha256}; @@ -13,7 +13,7 @@ fn generate_image_name( let patch_hash = format!("{:x}", Sha256::digest(patch_content)); // Create vLLM source identifier - let vllm_source_id = format!("{}/{}@{}", vllm_org, vllm_repo, vllm_branch); + let vllm_source_id = format!("{}/{}@{}", vllm_org, vllm_repo, vllm_sha); // Combine and hash (GCE image names have 63 char limit, format is "vllm-spnl-{hash}") let combined = format!("{}{}", patch_hash, vllm_source_id); @@ -41,12 +41,12 @@ pub struct ImageCreateArgs { #[builder(setter(into), default = "vllm".to_string())] pub(crate) vllm_repo: String, - /// vLLM branch to use - #[builder(setter(into), default = "llm-d-release-0.4".to_string())] - pub(crate) vllm_branch: String, + /// vLLM commit SHA to use + #[builder(setter(into), default = "a1b2c3d4e5f6".to_string())] + pub(crate) vllm_sha: String, /// LLM-D version for patch file - #[builder(setter(into), default = "0.4.0".to_string())] + #[builder(setter(into), default = "0.5.0".to_string())] pub(crate) llmd_version: String, /// Custom image name (defaults to auto-generated from hash) @@ -66,7 +66,7 @@ pub struct ImageCreateArgs { fn generate_startup_script( vllm_org: &str, vllm_repo: &str, - vllm_branch: &str, + vllm_sha: &str, patch_content_b64: &str, ) -> String { format!( @@ -97,8 +97,10 @@ sudo resize2fs /dev/sda1 2>/dev/null || true echo "=== Installing vLLM ===" curl -LsSf https://astral.sh/uv/install.sh | sh source $HOME/.local/bin/env -git clone https://github.com/{}/{}.git vllm -b {} +git clone https://github.com/{}/{}.git vllm cd vllm +git fetch origin {} +git checkout {} echo "=== Applying vLLM patch ===" # Decode the embedded patch file @@ -179,7 +181,7 @@ sudo rm -rf /var/lib/apt/lists/* echo "=== Image preparation complete ===" "#, - vllm_org, vllm_repo, vllm_branch, patch_content_b64 + vllm_org, vllm_repo, vllm_sha, vllm_sha, patch_content_b64 ) } @@ -423,7 +425,7 @@ struct ImageCreationParams<'a> { project: &'a str, vllm_org: &'a str, vllm_repo: &'a str, - vllm_branch: &'a str, + vllm_sha: &'a str, llmd_version: &'a str, } @@ -435,8 +437,8 @@ async fn create_image_from_disk(params: ImageCreationParams<'_>) -> anyhow::Resu let client = Images::builder().build().await?; let description = format!( - "vLLM custom image with VLLM_ORG={}, VLLM_REPO={}, VLLM_BRANCH={}, LLMD_VERSION={}", - params.vllm_org, params.vllm_repo, params.vllm_branch, params.llmd_version + "vLLM custom image with VLLM_ORG={}, VLLM_REPO={}, VLLM_SHA={}, LLMD_VERSION={}", + params.vllm_org, params.vllm_repo, params.vllm_sha, params.llmd_version ); eprintln!("Creating custom image: {}", params.image_name); @@ -560,7 +562,7 @@ pub async fn create_image(args: ImageCreateArgs) -> anyhow::Result { eprintln!("Configuration:"); eprintln!(" VLLM_ORG: {}", args.vllm_org); eprintln!(" VLLM_REPO: {}", args.vllm_repo); - eprintln!(" VLLM_BRANCH: {}", args.vllm_branch); + eprintln!(" VLLM_SHA: {}", args.vllm_sha); eprintln!(" LLMD_VERSION: {}", args.llmd_version); eprintln!(" IMAGE_FAMILY: {}", args.image_family); eprintln!(" IMAGE_PROJECT: {}", project); @@ -568,12 +570,12 @@ pub async fn create_image(args: ImageCreateArgs) -> anyhow::Result { // Embed patch file at compile time based on LLMD version let patch_content = match args.llmd_version.as_str() { - "0.4.0" => { - include_bytes!("../../../docker/vllm/llm-d/patches/0.4.0/01-spans-llmd-vllm.patch.gz") + "0.5.0" => { + include_bytes!("../../../docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz") } _ => { return Err(anyhow::anyhow!( - "Unsupported LLMD version: {}. Only 0.4.0 is currently supported.", + "Unsupported LLMD version: {}. Only 0.5 is currently supported.", args.llmd_version )); } @@ -592,7 +594,7 @@ pub async fn create_image(args: ImageCreateArgs) -> anyhow::Result { patch_content, &args.vllm_org, &args.vllm_repo, - &args.vllm_branch, + &args.vllm_sha, ) }; @@ -626,7 +628,7 @@ pub async fn create_image(args: ImageCreateArgs) -> anyhow::Result { let startup_script = generate_startup_script( &args.vllm_org, &args.vllm_repo, - &args.vllm_branch, + &args.vllm_sha, &patch_content_b64, ); @@ -654,7 +656,7 @@ pub async fn create_image(args: ImageCreateArgs) -> anyhow::Result { project: &project, vllm_org: &args.vllm_org, vllm_repo: &args.vllm_repo, - vllm_branch: &args.vllm_branch, + vllm_sha: &args.vllm_sha, llmd_version: &args.llmd_version, }) .await?; @@ -701,8 +703,8 @@ mod tests { .force_overwrite(true) .vllm_org("test-org") .vllm_repo("test-repo") - .vllm_branch("test-branch") - .llmd_version("0.4.0") + .vllm_sha("abc123def456") + .llmd_version("0.5.0") .image_family("test-family") .build() .unwrap(); @@ -710,8 +712,8 @@ mod tests { assert!(args.force_overwrite); assert_eq!(args.vllm_org, "test-org"); assert_eq!(args.vllm_repo, "test-repo"); - assert_eq!(args.vllm_branch, "test-branch"); - assert_eq!(args.llmd_version, "0.4.0"); + assert_eq!(args.vllm_sha, "abc123def456"); + assert_eq!(args.llmd_version, "0.5.0"); assert_eq!(args.image_family, "test-family"); } } diff --git a/spnl/src/vllm/gce/up.rs b/spnl/src/vllm/gce/up.rs index e55df6d1..c5decf3c 100644 --- a/spnl/src/vllm/gce/up.rs +++ b/spnl/src/vllm/gce/up.rs @@ -71,7 +71,7 @@ fn load_cloud_config(args: &UpArgs) -> anyhow::Result { }; let vllm_org = &args.config.vllm_org; let vllm_repo = &args.config.vllm_repo; - let vllm_branch = &args.config.vllm_branch; + let vllm_sha = &args.config.vllm_sha; let model = args .model .clone() @@ -187,7 +187,7 @@ cloud_final_modules: []"# substitutions.insert("spnl_release", spnl_release.as_str()); substitutions.insert("vllm_org", vllm_org.as_str()); substitutions.insert("vllm_repo", vllm_repo.as_str()); - substitutions.insert("vllm_branch", vllm_branch.as_str()); + substitutions.insert("vllm_sha", vllm_sha.as_str()); substitutions.insert("model", model.as_str()); substitutions.insert("packages_section", &packages_section); substitutions.insert("setup_dev_script", &setup_dev_script); @@ -248,11 +248,19 @@ pub async fn up(args: UpArgs) -> anyhow::Result<()> { // Determine which image to use let source_image = if is_dev_mode { // Dev mode: use standard Ubuntu accelerator image - "projects/ubuntu-os-accelerator-images/global/images/ubuntu-accelerator-2404-amd64-with-nvidia-580-v20251210".to_string() + "projects/ubuntu-os-accelerator-images/global/images/ubuntu-accelerator-2404-amd64-with-nvidia-580-v20260203".to_string() } else { // Production mode: use custom image based on vLLM configuration - // Use the image family to get the latest image - format!("projects/{}/global/images/family/vllm-spnl", project) + // Generate the exact image name using the same logic as image creation + let patch_content = + include_bytes!("../../../docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz"); + let image_name = super::image::generate_image_name( + patch_content, + &args.config.vllm_org, + &args.config.vllm_repo, + &args.config.vllm_sha, + ); + format!("projects/{}/global/images/{}", project, image_name) }; #[derive(Tabled)] @@ -314,6 +322,15 @@ pub async fn up(args: UpArgs) -> anyhow::Result<()> { "v2-x86-template-1-4-0".to_string(), ); + // Configure disk based on mode + // Dev mode needs more space for compilation (300GB pd-balanced) + // Production mode uses smaller, faster disk (100GB pd-ssd) + let (disk_size_gb, disk_type) = if is_dev_mode { + (300, format!("zones/{}/diskTypes/pd-balanced", zone)) + } else { + (100, format!("zones/{}/diskTypes/pd-ssd", zone)) + }; + // Create the instance configuration matching the terraform file let instance = Instance::new() .set_name(&instance_name) @@ -325,8 +342,8 @@ pub async fn up(args: UpArgs) -> anyhow::Result<()> { .set_initialize_params( AttachedDiskInitializeParams::new() .set_source_image(&source_image) - .set_disk_size_gb(100) - .set_disk_type(format!("zones/{}/diskTypes/pd-ssd", zone)), + .set_disk_size_gb(disk_size_gb) + .set_disk_type(disk_type), ) .set_mode("READ_WRITE")]) .set_network_interfaces([NetworkInterface::new() diff --git a/spnl/src/vllm/patch.rs b/spnl/src/vllm/patch.rs index 2d3a7424..d424d660 100644 --- a/spnl/src/vllm/patch.rs +++ b/spnl/src/vllm/patch.rs @@ -1,7 +1,7 @@ use std::io::{Read, Write}; const PATCH_DATA: &[u8] = - include_bytes!("../../docker/vllm/llm-d/patches/0.4.0/01-spans-llmd-vllm.patch.gz"); + include_bytes!("../../docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz"); /// Emit the vLLM patchfile to stdout /// From c597861b56f0255833d80a6794a4bcd8bfccf76e Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Mon, 16 Feb 2026 19:01:22 -0500 Subject: [PATCH 2/3] fix(vllm-gce): Add VLLM_PRECOMPILED_WHEEL_COMMIT parameter to fix ABI compatibility - Add vllm_precompiled_wheel_commit field to GceConfig with default value from llm-d - Update setup-dev.sh to use VLLM_PRECOMPILED_WHEEL_COMMIT for wheel lookup - Propagate parameter through cloud-config.yaml and up.rs substitutions - Fixes ImportError: undefined symbol _ZN3c104cuda29c10_cuda_check_implementationEiPKcS2_jb - Ensures precompiled wheels match PyTorch/CUDA environment ABI Signed-off-by: Nick Mitchell --- docker/gce/vllm/cloud-config.yaml | 1 + docker/gce/vllm/setup-dev.sh | 40 ++++++++++++++++++++++++++++++- spnl/src/vllm/gce/args.rs | 15 ++++++++++++ spnl/src/vllm/gce/up.rs | 5 ++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/docker/gce/vllm/cloud-config.yaml b/docker/gce/vllm/cloud-config.yaml index 4b9eb474..9e37f038 100644 --- a/docker/gce/vllm/cloud-config.yaml +++ b/docker/gce/vllm/cloud-config.yaml @@ -24,6 +24,7 @@ write_files: VLLM_ORG=${vllm_org} VLLM_REPO=${vllm_repo} VLLM_SHA=${vllm_sha} + VLLM_PRECOMPILED_WHEEL_COMMIT=${vllm_precompiled_wheel_commit} MODEL=${model} VLLM_PATCHFILE=/tmp/vllm.patch ${vllm_config_section} diff --git a/docker/gce/vllm/setup-dev.sh b/docker/gce/vllm/setup-dev.sh index 1298bcda..742b9feb 100644 --- a/docker/gce/vllm/setup-dev.sh +++ b/docker/gce/vllm/setup-dev.sh @@ -75,7 +75,45 @@ git fetch origin $VLLM_SHA git checkout $VLLM_SHA uv venv --seed source .venv/bin/activate -VLLM_USE_PRECOMPILED=1 uv pip install --editable . + +# Default VLLM_PRECOMPILED_WHEEL_COMMIT to VLLM_SHA if not set +# This allows using precompiled binaries from a different commit (e.g., main) while checking out a specific source commit +VLLM_PRECOMPILED_WHEEL_COMMIT="${VLLM_PRECOMPILED_WHEEL_COMMIT:-${VLLM_SHA}}" + +# Detect if precompiled wheel exists (following llm-d approach) +MACHINE=$(uname -m) +case "${MACHINE}" in + x86_64|amd64) PLATFORM_TAG="manylinux_2_31_x86_64" ;; + aarch64|arm64) PLATFORM_TAG="manylinux_2_31_aarch64" ;; + *) echo "Unsupported architecture: ${MACHINE}"; PLATFORM_TAG="" ;; +esac + +WHEEL_URL="" +if [ -n "${PLATFORM_TAG}" ]; then + echo "Looking for precompiled wheel at: https://wheels.vllm.ai/${VLLM_PRECOMPILED_WHEEL_COMMIT}/vllm/" + WHEEL_INDEX_HTML=$(curl -sf "https://wheels.vllm.ai/${VLLM_PRECOMPILED_WHEEL_COMMIT}/vllm/" 2>/dev/null || echo "") + if [ -n "${WHEEL_INDEX_HTML}" ]; then + WHEEL_FILENAME=$(echo "${WHEEL_INDEX_HTML}" | grep -oE "vllm-[^\"]+${PLATFORM_TAG}\.whl" | head -1) + if [ -n "${WHEEL_FILENAME}" ]; then + # URL-encode the + sign in the wheel filename + WHEEL_URL="https://wheels.vllm.ai/${VLLM_PRECOMPILED_WHEEL_COMMIT}/${WHEEL_FILENAME}" + WHEEL_URL=$(echo "${WHEEL_URL}" | sed -E 's/\+/%2B/g') + echo "Found precompiled wheel: ${WHEEL_URL}" + fi + fi +fi + +# Install vLLM with or without precompiled binaries +if [ -n "${WHEEL_URL}" ]; then + echo "Using precompiled binaries from commit: ${VLLM_PRECOMPILED_WHEEL_COMMIT} (source: ${VLLM_SHA})" + export VLLM_USE_PRECOMPILED=1 + export VLLM_PRECOMPILED_WHEEL_LOCATION="${WHEEL_URL}" + uv pip install --editable . +else + echo "Compiling vLLM from source (no precompiled wheel found or unsupported platform)" + unset VLLM_USE_PRECOMPILED VLLM_PRECOMPILED_WHEEL_LOCATION 2>/dev/null || true + uv pip install --editable . +fi # Wait for spnl build to complete echo "Waiting for spnl build to complete..." diff --git a/spnl/src/vllm/gce/args.rs b/spnl/src/vllm/gce/args.rs index bf60dc5b..2ffc2f2e 100644 --- a/spnl/src/vllm/gce/args.rs +++ b/spnl/src/vllm/gce/args.rs @@ -55,6 +55,16 @@ pub struct GceConfig { /// vLLM commit SHA to use #[arg(long, env = "VLLM_SHA", default_value = "a1b2c3d4e5f6")] pub vllm_sha: String, + + /// vLLM commit SHA to use for precompiled wheel lookup (defaults to vllm_sha) + /// This allows using stable precompiled binaries from a known commit (e.g., main) + /// while checking out a different source commit for testing + #[arg( + long, + env = "VLLM_PRECOMPILED_WHEEL_COMMIT", + default_value = "d7de043d55d1dd629554467e23874097e1c48993" + )] + pub vllm_precompiled_wheel_commit: String, } impl GceConfig { @@ -73,6 +83,7 @@ impl GceConfig { vllm_org: "neuralmagic".to_string(), vllm_repo: "vllm".to_string(), vllm_sha: "a1b2c3d4e5f6".to_string(), + vllm_precompiled_wheel_commit: "d7de043d55d1dd629554467e23874097e1c48993".to_string(), } } @@ -127,6 +138,10 @@ mod tests { assert_eq!(config.vllm_org, "neuralmagic"); assert_eq!(config.vllm_repo, "vllm"); assert_eq!(config.vllm_sha, "a1b2c3d4e5f6"); + assert_eq!( + config.vllm_precompiled_wheel_commit, + "d7de043d55d1dd629554467e23874097e1c48993" + ); } #[test] diff --git a/spnl/src/vllm/gce/up.rs b/spnl/src/vllm/gce/up.rs index c5decf3c..fd1e5e93 100644 --- a/spnl/src/vllm/gce/up.rs +++ b/spnl/src/vllm/gce/up.rs @@ -72,6 +72,7 @@ fn load_cloud_config(args: &UpArgs) -> anyhow::Result { let vllm_org = &args.config.vllm_org; let vllm_repo = &args.config.vllm_repo; let vllm_sha = &args.config.vllm_sha; + let vllm_precompiled_wheel_commit = &args.config.vllm_precompiled_wheel_commit; let model = args .model .clone() @@ -188,6 +189,10 @@ cloud_final_modules: []"# substitutions.insert("vllm_org", vllm_org.as_str()); substitutions.insert("vllm_repo", vllm_repo.as_str()); substitutions.insert("vllm_sha", vllm_sha.as_str()); + substitutions.insert( + "vllm_precompiled_wheel_commit", + vllm_precompiled_wheel_commit.as_str(), + ); substitutions.insert("model", model.as_str()); substitutions.insert("packages_section", &packages_section); substitutions.insert("setup_dev_script", &setup_dev_script); From e1962065f71a710758c3b20886f1cfaec1094639 Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Tue, 17 Feb 2026 09:41:46 -0500 Subject: [PATCH 3/3] fix: updated 0.5.0 patch from opus 4.6 Signed-off-by: Nick Mitchell --- docker/vllm/llm-d/Containerfile.cuda | 4 ++-- .../patches/0.5.0/01-spans-llmd-vllm.patch.gz | Bin 10847 -> 10991 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/vllm/llm-d/Containerfile.cuda b/docker/vllm/llm-d/Containerfile.cuda index 6f8f4812..438c52bb 100644 --- a/docker/vllm/llm-d/Containerfile.cuda +++ b/docker/vllm/llm-d/Containerfile.cuda @@ -1,4 +1,4 @@ -ARG LLMD_VERSION=0.4.0 +ARG LLMD_VERSION=0.5.0 ARG MANYLINUX_VERSION=2_34 # use 2_39 for aarch64; TODO is it possible to infer this from the build platform? # Python version extractor @@ -29,7 +29,7 @@ LABEL org.opencontainers.image.source=https://github.com/IBM/spnl LABEL org.opencontainers.image.description="Span Query support for llm-d's vLLM" LABEL org.opencontainers.image.licenses="Apache-2.0" -ARG LLMD_VERSION=0.4.0 # sigh, we need to repeat this if we want to use it inside of the FROM +ARG LLMD_VERSION=0.5.0 # sigh, we need to repeat this if we want to use it inside of the FROM COPY --from=builder target/wheels/ /tmp/wheels COPY docker/vllm/llm-d/patches/$LLMD_VERSION/ /tmp/patches diff --git a/docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz b/docker/vllm/llm-d/patches/0.5.0/01-spans-llmd-vllm.patch.gz index 745d72dd45c73427bd03c39bc7a5c09a75e9796e..e7fa2e27451225ce14ca65c4f9272920f66375bc 100644 GIT binary patch literal 10991 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 literal 10847 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