236 lines
8.2 KiB
Bash
236 lines
8.2 KiB
Bash
#!/bin/bash
|
||
# SPDX-License-Identifier: Apache-2.0
|
||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||
#
|
||
# Check if Ray LLM can generate lock files that are compatible with this
|
||
# version of vllm. Downloads Ray's requirement files and runs a full
|
||
# dependency resolution with the installed vllm's constraints to see if
|
||
# a valid lock file can be produced.
|
||
#
|
||
# See: https://github.com/vllm-project/vllm/issues/33599
|
||
|
||
set -eo pipefail
|
||
|
||
RAY_BASE_URL="https://raw.githubusercontent.com/ray-project/ray/master/python"
|
||
|
||
WORK_DIR=$(mktemp -d)
|
||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||
|
||
# ── Detect PyTorch index URL ─────────────────────────────────────────────
|
||
|
||
if python3 -c "import torch; assert torch.version.hip" 2>/dev/null; then
|
||
ROCM_VER=$(python3 -c "import torch; print(torch.version.hip.rsplit('.', 1)[0])")
|
||
CANDIDATE_URL="https://download.pytorch.org/whl/rocm${ROCM_VER}"
|
||
if curl -fsSL --head "${CANDIDATE_URL}/" >/dev/null 2>&1; then
|
||
TORCH_INDEX_URL="${CANDIDATE_URL}"
|
||
else
|
||
echo ">>> WARNING: ROCm ${ROCM_VER} wheel index not found at ${CANDIDATE_URL}"
|
||
echo ">>> Falling back to default PyPI (resolution may be incomplete)"
|
||
TORCH_INDEX_URL=""
|
||
fi
|
||
else
|
||
TORCH_INDEX_URL="https://download.pytorch.org/whl/cu129"
|
||
fi
|
||
echo ">>> Using PyTorch index: ${TORCH_INDEX_URL:-PyPI default}"
|
||
|
||
# Fetch all Ray requirement files used in the LLM depset pipeline
|
||
echo ">>> Fetching Ray requirement files"
|
||
RAY_FILES=(
|
||
"requirements.txt"
|
||
"requirements/cloud-requirements.txt"
|
||
"requirements/base-test-requirements.txt"
|
||
"requirements/llm/llm-requirements.txt"
|
||
"requirements/llm/llm-test-requirements.txt"
|
||
)
|
||
for FILE in "${RAY_FILES[@]}"; do
|
||
LOCAL_PATH="${WORK_DIR}/$(basename "$FILE")"
|
||
echo " ${FILE}"
|
||
curl -fsSL -o "$LOCAL_PATH" "${RAY_BASE_URL}/${FILE}"
|
||
done
|
||
|
||
# Extract installed vllm deps
|
||
echo ">>> Extracting installed vllm dependency constraints"
|
||
python3 - "${WORK_DIR}/vllm-constraints.txt" <<'PYEOF'
|
||
"""Write out the installed vllm's dependencies as pip constraint lines.
|
||
|
||
Ray uses vllm[audio], so audio-extra deps are included with their extra
|
||
markers stripped. The resolver cannot evaluate extra markers for a
|
||
package that is not itself being resolved from an index, so we activate
|
||
them manually here.
|
||
"""
|
||
import importlib.metadata
|
||
import re
|
||
import sys
|
||
|
||
out_path = sys.argv[1]
|
||
raw_reqs = importlib.metadata.requires("vllm") or []
|
||
|
||
# Ray uses vllm[audio] – activate that extra.
|
||
ACTIVE_EXTRAS = {"audio"}
|
||
EXTRA_RE = re.compile(r"""extra\s*==\s*['"]([^'"]+)['"]""")
|
||
|
||
lines = []
|
||
for r in raw_reqs:
|
||
if ";" not in r:
|
||
# Unconditional dep — always include.
|
||
lines.append(r.strip())
|
||
continue
|
||
|
||
req_part, _, marker_part = r.partition(";")
|
||
marker_part = marker_part.strip()
|
||
|
||
extra_matches = EXTRA_RE.findall(marker_part)
|
||
if not extra_matches:
|
||
# Non-extra marker (python_version, etc.) — keep as-is.
|
||
lines.append(r.strip())
|
||
continue
|
||
|
||
if not ACTIVE_EXTRAS.intersection(extra_matches):
|
||
continue # Skip inactive extras (tensorizer, bench, …).
|
||
|
||
# Strip the extra== conditions but keep any remaining markers
|
||
# (e.g. python_version).
|
||
cleaned = EXTRA_RE.sub("", marker_part)
|
||
cleaned = re.sub(r"\band\b\s*\band\b", "and", cleaned)
|
||
cleaned = re.sub(r"^\s*and\s+|\s+and\s*$", "", cleaned).strip()
|
||
|
||
if cleaned:
|
||
lines.append(f"{req_part.strip()} ; {cleaned}")
|
||
else:
|
||
lines.append(req_part.strip())
|
||
|
||
with open(out_path, "w") as f:
|
||
for line in lines:
|
||
f.write(line + "\n")
|
||
|
||
print(f"Wrote {len(lines)} constraints to {out_path}")
|
||
PYEOF
|
||
|
||
echo ">>> Installed vllm deps (first 20 lines):"
|
||
head -20 "${WORK_DIR}/vllm-constraints.txt"
|
||
|
||
# Remove Ray's vllm pin — the installed vllm's transitive deps
|
||
# (written above) replace it in the resolution. vllm itself cannot
|
||
# be resolved from PyPI for in-development versions, so we test
|
||
# whether Ray's requirements can coexist with vllm's dependency
|
||
# constraints instead.
|
||
sed -i '/^vllm/d' "${WORK_DIR}/llm-requirements.txt"
|
||
|
||
# Install uv if needed
|
||
if ! command -v uv &>/dev/null; then
|
||
echo ">>> Installing uv"
|
||
pip install uv -q
|
||
fi
|
||
|
||
# Resolve: given vllm's constraints, can Ray compile a lock file?
|
||
#
|
||
# vllm's dependency constraints are the fixed side — Ray is flexible and
|
||
# can regenerate its lock files. We pass vllm's constraints via -c so
|
||
# the resolver treats them as non-negotiable bounds, then check whether
|
||
# Ray's own requirements can still be satisfied within those bounds.
|
||
echo ""
|
||
echo "============================================================"
|
||
echo ">>> Resolving: Can Ray generate compatible lock files?"
|
||
echo "============================================================"
|
||
|
||
EXTRA_INDEX_ARGS=()
|
||
if [[ -n "${TORCH_INDEX_URL}" ]]; then
|
||
EXTRA_INDEX_ARGS+=(--extra-index-url "${TORCH_INDEX_URL}")
|
||
fi
|
||
|
||
set +e
|
||
uv pip compile \
|
||
"${WORK_DIR}/requirements.txt" \
|
||
"${WORK_DIR}/cloud-requirements.txt" \
|
||
"${WORK_DIR}/base-test-requirements.txt" \
|
||
"${WORK_DIR}/llm-requirements.txt" \
|
||
"${WORK_DIR}/llm-test-requirements.txt" \
|
||
-c "${WORK_DIR}/vllm-constraints.txt" \
|
||
--python-version 3.12 \
|
||
--python-platform x86_64-manylinux_2_31 \
|
||
"${EXTRA_INDEX_ARGS[@]}" \
|
||
--index-strategy unsafe-best-match \
|
||
--unsafe-package setuptools \
|
||
--unsafe-package ray \
|
||
--no-header \
|
||
-o "${WORK_DIR}/resolved.txt" \
|
||
2>&1
|
||
EXIT_CODE=$?
|
||
set -e
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
if [ $EXIT_CODE -eq 0 ]; then
|
||
echo "SUCCESS: Ray can generate lock files compatible with this vllm."
|
||
echo ""
|
||
echo "Key resolved versions:"
|
||
grep -E '^(protobuf|torch|numpy|transformers)==' \
|
||
"${WORK_DIR}/resolved.txt" | sort || true
|
||
echo "=========================================="
|
||
exit 0
|
||
fi
|
||
|
||
echo "FAILURE: Ray cannot generate lock files compatible with this vllm."
|
||
echo "This means a fundamental dependency conflict exists that Ray"
|
||
echo "cannot resolve by regenerating its lock files."
|
||
echo "See: https://github.com/vllm-project/vllm/issues/33599"
|
||
echo "=========================================="
|
||
|
||
# Buildkite annotation
|
||
if [ -f /usr/bin/buildkite-agent ]; then
|
||
buildkite-agent annotate --style 'warning' --context 'ray-compat' << EOF
|
||
### :warning: Ray Dependency Compatibility Warning
|
||
This PR introduces dependencies that **cannot** be resolved with Ray's requirements.
|
||
Ray would not be able to regenerate its lock files to accommodate this vllm version.
|
||
|
||
Please check the **Ray Dependency Compatibility Check** step logs for details.
|
||
See [issue #33599](https://github.com/vllm-project/vllm/issues/33599) for context.
|
||
EOF
|
||
fi
|
||
|
||
# Notify Slack if webhook is configured and PR/branch are valid.
|
||
if [ -n "$RAY_COMPAT_SLACK_WEBHOOK_URL" ]; then
|
||
PR="${BUILDKITE_PULL_REQUEST:-}"
|
||
BRANCH="${BUILDKITE_BRANCH:-}"
|
||
|
||
# Skip notification if PR is invalid or branch is empty
|
||
if [[ "$PR" = "false" || -z "$PR" || -z "$BRANCH" ]]; then
|
||
echo ">>> Skipping Slack notification (invalid PR or empty branch: PR=$PR, branch=$BRANCH)"
|
||
else
|
||
echo ">>> Sending Slack notification"
|
||
# Single quotes are intentional: the f-string expressions are Python, not shell.
|
||
# shellcheck disable=SC2016
|
||
PAYLOAD=$(python3 -c '
|
||
import json, os, sys
|
||
pr = os.getenv("BUILDKITE_PULL_REQUEST", "N/A")
|
||
branch = os.getenv("BUILDKITE_BRANCH", "unknown")
|
||
url = os.getenv("BUILDKITE_BUILD_URL", "#")
|
||
data = {
|
||
"text": ":warning: Ray Dependency Compatibility Check Failed",
|
||
"blocks": [{
|
||
"type": "section",
|
||
"text": {
|
||
"type": "mrkdwn",
|
||
"text": (
|
||
"*:warning: Ray Dependency Compatibility Check Failed*\n"
|
||
f"PR #{pr} on branch `{branch}` introduces dependencies "
|
||
f"that cannot be resolved with Ray'\''s requirements.\n"
|
||
f"<{url}|View Build>"
|
||
),
|
||
},
|
||
}],
|
||
}
|
||
print(json.dumps(data))
|
||
')
|
||
|
||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$RAY_COMPAT_SLACK_WEBHOOK_URL" \
|
||
-H 'Content-type: application/json' \
|
||
-d "$PAYLOAD")
|
||
echo " Slack webhook response: $HTTP_CODE"
|
||
fi
|
||
else
|
||
echo ">>> Skipping Slack notification (RAY_COMPAT_SLACK_WEBHOOK_URL not set)"
|
||
fi
|
||
|
||
exit 1
|