[Release] Make SM70 wheel delivery self-contained
Bundle the release launcher in the source build stage, pin the CUDA 12.8 runtime dependency for SM70 wheels, and validate wheel metadata, ZIP paths, and native loader paths before publication. Assisted-by: OpenAI Codex Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
This commit is contained in:
1 parent
c5cbf1e247
commit
6f6fc4c527
6 files changed
+192
-11
No files matched your search
@@ -89,8 +89,8 @@ steps:
|
||||
- "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.8.1 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_SM70}\" --build-arg BUILD_OS=manylinux --build-arg BUILD_BASE_IMAGE=pytorch/manylinux2_28-builder:cuda12.8 --tag vllm-ci:build-image-sm70 --target build --progress plain -f docker/Dockerfile ."
|
||||
- "mkdir artifacts"
|
||||
- "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image-sm70 bash -c 'python3 tools/check_sm70_release_artifact.py dist/*.whl && cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'"
|
||||
- "bash .buildkite/scripts/upload-nightly-wheels.sh"
|
||||
- 'bash .buildkite/scripts/annotate-build-artifact.sh "$$BUILDKITE_LABEL" "s3://vllm-wheels/$$BUILDKITE_COMMIT/$(cd artifacts/dist && echo *.whl)"'
|
||||
- 'buildkite-agent artifact upload "artifacts/dist/*.whl"'
|
||||
- 'bash .buildkite/scripts/annotate-build-artifact.sh "$$BUILDKITE_LABEL" "Buildkite artifact: artifacts/dist/*.whl"'
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
|
||||
@@ -1024,6 +1024,26 @@ verifier out of its fast path.
|
||||
|
||||
---
|
||||
|
||||
# 📦 Install the SM70 release wheel
|
||||
|
||||
For the Qwen3.8-27B NVFP4 + DFlash2 V100 profile, install the release wheel
|
||||
into a clean Python 3.12 environment. The wheel carries the SM70 extensions,
|
||||
Flash-V100, FlashQLA, and the versioned launcher; its metadata selects the
|
||||
CUDA 12.8 PyTorch wheels for this profile.
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
. .venv/bin/activate
|
||||
python -m pip install ./1cat_vllm-<version>-cp312-cp312-manylinux_2_28_x86_64.whl
|
||||
serve_qwen38_27b_nvfp4_v100 /models/Qwen3.8-27B-NVFP4
|
||||
```
|
||||
|
||||
The launcher uses the installed `vllm` command and packaged extensions. It
|
||||
does not require a checkout path, a copied `.so`, or `VLLM_*`/`FLASH_*`
|
||||
environment overrides. Four peer-connected V100-SXM2 32GB GPUs are required
|
||||
for this profile; pass normal `vllm serve` options after the model path when
|
||||
changing the service port or resource limits.
|
||||
|
||||
# 🔨 Build From Source
|
||||
|
||||
Clone:
|
||||
@@ -1042,6 +1062,10 @@ export CMAKE_CUDA_ARCHITECTURES=70
|
||||
|
||||
Then build/install the project using the repository's current build instructions for your CUDA/PyTorch environment.
|
||||
|
||||
The two variables above are build-time inputs for a source build only. They
|
||||
are already fixed in the SM70 release-wheel build and are not needed after
|
||||
installing that wheel.
|
||||
|
||||
Because this project contains custom CUDA extensions, make sure the active compiler/toolkit matches the PyTorch CUDA ABI used by your environment.
|
||||
|
||||
---
|
||||
|
||||
@@ -342,6 +342,7 @@ RUN --mount=type=cache,target=/opt/uv/cache \
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY pyproject.toml setup.py CMakeLists.txt ./
|
||||
COPY scripts/serve_qwen38_27b_nvfp4_v100.sh scripts/
|
||||
COPY cmake cmake/
|
||||
COPY csrc csrc/
|
||||
COPY flash_qla flash_qla/
|
||||
|
||||
@@ -1207,9 +1207,18 @@ def get_requirements() -> list[str]:
|
||||
elif _is_cuda():
|
||||
requirements = _read_requirements("cuda.txt")
|
||||
cuda_major, cuda_minor = torch.version.cuda.split(".")
|
||||
# An SM70 wheel is a CUDA 12.8 release artifact. Keep its runtime
|
||||
# dependency on the matching PyTorch wheel in the wheel metadata so a
|
||||
# user installing the wheel does not need to know about this build
|
||||
# detail or export a local-environment override. The explicit env
|
||||
# switch remains for reproducible packaging jobs that pin the same
|
||||
# dependency while using a non-SM70 target.
|
||||
pin_torch_cu128 = bool(int(os.getenv("ONECAT_VLLM_PIN_TORCH_CU128", "0"))) or (
|
||||
_cuda_arch_contains(7, 0) and torch.version.cuda == "12.8"
|
||||
)
|
||||
modified_requirements = []
|
||||
for req in requirements:
|
||||
if bool(int(os.getenv("ONECAT_VLLM_PIN_TORCH_CU128", "0"))):
|
||||
if pin_torch_cu128:
|
||||
req = ONECAT_TORCH_CU128_URLS.get(req.split("#", 1)[0].strip(), req)
|
||||
if "vllm-flash-attn" in req and cuda_major != "12":
|
||||
# vllm-flash-attn is built only for CUDA 12.x.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -31,6 +33,12 @@ def write_wheel(path: Path, members: list[str]) -> None:
|
||||
with zipfile.ZipFile(path, "w") as wheel:
|
||||
for member in members:
|
||||
wheel.writestr(member, b"fixture; dynamic inspection is disabled")
|
||||
wheel.writestr(
|
||||
"1cat_vllm-0.0.0.dist-info/WHEEL",
|
||||
"Wheel-Version: 1.0\n"
|
||||
"Generator: test\n"
|
||||
"Tag: cp312-cp312-manylinux_2_28_x86_64\n",
|
||||
)
|
||||
|
||||
|
||||
def test_complete_sm70_wheel_manifest_passes(tmp_path: Path) -> None:
|
||||
@@ -66,6 +74,77 @@ def test_launcher_is_required(tmp_path: Path) -> None:
|
||||
check_wheel(wheel, inspect_dynamic=False)
|
||||
|
||||
|
||||
def test_release_wheel_requires_cp312_tag(tmp_path: Path) -> None:
|
||||
wheel = tmp_path / "wrong-tag.whl"
|
||||
with zipfile.ZipFile(wheel, "w") as archive:
|
||||
for member in REQUIRED_MEMBERS:
|
||||
archive.writestr(member, b"fixture")
|
||||
archive.writestr(
|
||||
"1cat_vllm-0.0.0.dist-info/WHEEL",
|
||||
"Wheel-Version: 1.0\n"
|
||||
"Generator: test\n"
|
||||
"Tag: cp311-cp311-manylinux_2_28_x86_64\n",
|
||||
)
|
||||
|
||||
with pytest.raises(ArtifactError, match="Python 3.12"):
|
||||
check_wheel(wheel, inspect_dynamic=False)
|
||||
|
||||
|
||||
def test_unsafe_zip_member_is_rejected_before_dynamic_inspection(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
wheel = tmp_path / "unsafe.whl"
|
||||
write_wheel(wheel, [*REQUIRED_MEMBERS, "../outside.so"])
|
||||
|
||||
def fail_if_called(*args, **kwargs):
|
||||
pytest.fail("dynamic inspection must not extract unsafe members")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tools.check_sm70_release_artifact._check_dynamic_dependencies",
|
||||
fail_if_called,
|
||||
)
|
||||
with pytest.raises(ArtifactError, match="unsafe wheel member"):
|
||||
check_wheel(wheel)
|
||||
|
||||
|
||||
def test_dynamic_inspection_rejects_private_rpath(tmp_path: Path) -> None:
|
||||
compiler = shutil.which("cc")
|
||||
if compiler is None:
|
||||
pytest.skip("cc is required for the native artifact fixture")
|
||||
assert compiler is not None
|
||||
|
||||
source = tmp_path / "fixture.c"
|
||||
source.write_text("int fixture(void) { return 1; }\n", encoding="utf-8")
|
||||
native = tmp_path / "fixture.so"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-shared",
|
||||
"-fPIC",
|
||||
str(source),
|
||||
"-Wl,-rpath,/home/private-build/lib",
|
||||
"-o",
|
||||
str(native),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
wheel = tmp_path / "private-rpath.whl"
|
||||
with zipfile.ZipFile(wheel, "w") as archive:
|
||||
payload = native.read_bytes()
|
||||
for member in REQUIRED_MEMBERS:
|
||||
archive.writestr(member, payload)
|
||||
archive.writestr(
|
||||
"1cat_vllm-0.0.0.dist-info/WHEEL",
|
||||
"Wheel-Version: 1.0\n"
|
||||
"Generator: test\n"
|
||||
"Tag: cp312-cp312-manylinux_2_28_x86_64\n",
|
||||
)
|
||||
|
||||
with pytest.raises(ArtifactError, match="private build path|RPATH"):
|
||||
check_wheel(wheel)
|
||||
|
||||
|
||||
def test_release_launcher_has_no_developer_runtime_overlays() -> None:
|
||||
launcher = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
|
||||
@@ -16,7 +16,7 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
import regex as re
|
||||
|
||||
@@ -69,6 +69,8 @@ REQUIRED_MEMBERS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
||||
)
|
||||
|
||||
LAUNCHER_SUFFIX = ".data/scripts/serve_qwen38_27b_nvfp4_v100.sh"
|
||||
WHEEL_METADATA_PATTERN = re.compile(r"^[^/]+\.dist-info/WHEEL$")
|
||||
RELEASE_PYTHON_TAG = "cp312"
|
||||
PRIVATE_PATH_MARKERS = (
|
||||
"/home/",
|
||||
"/data/",
|
||||
@@ -87,6 +89,37 @@ def _dynamic_section_uses_private_path(output: str) -> bool:
|
||||
return any(marker in lowered for marker in PRIVATE_PATH_MARKERS)
|
||||
|
||||
|
||||
def _dynamic_section_errors(member: str, output: str) -> list[str]:
|
||||
"""Reject loader paths that escape the wheel or name an absolute DSO."""
|
||||
|
||||
errors: list[str] = []
|
||||
if _dynamic_section_uses_private_path(output):
|
||||
errors.append(f"{member}: private build path in dynamic section")
|
||||
|
||||
for line in output.splitlines():
|
||||
if "(RPATH)" not in line and "(RUNPATH)" not in line:
|
||||
continue
|
||||
match = re.search(r"\[(.*?)\]", line)
|
||||
if match is None:
|
||||
continue
|
||||
for path in match.group(1).split(":"):
|
||||
path = path.strip()
|
||||
if not path:
|
||||
continue
|
||||
if path.startswith("$ORIGIN") or path.startswith("${ORIGIN}"):
|
||||
continue
|
||||
errors.append(f"{member}: non-portable RPATH/RUNPATH entry {path!r}")
|
||||
|
||||
# A DT_NEEDED entry with a slash is an absolute or caller-relative loader
|
||||
# path. A wheel may rely on system CUDA/driver libraries by soname, but it
|
||||
# must not reach back into the build host for a private .so.
|
||||
for match in re.finditer(r"Shared library: \[(.*?)\]", output):
|
||||
needed = match.group(1)
|
||||
if "/" in needed or "\\" in needed:
|
||||
errors.append(f"{member}: non-portable DT_NEEDED entry {needed!r}")
|
||||
return errors
|
||||
|
||||
|
||||
def _check_dynamic_dependencies(
|
||||
wheel: zipfile.ZipFile,
|
||||
native_members: list[str],
|
||||
@@ -111,8 +144,7 @@ def _check_dynamic_dependencies(
|
||||
if result.returncode != 0:
|
||||
errors.append(f"{member}: readelf failed: {result.stderr.strip()}")
|
||||
continue
|
||||
if _dynamic_section_uses_private_path(result.stdout):
|
||||
errors.append(f"{member}: private build path in RPATH/RUNPATH")
|
||||
errors.extend(_dynamic_section_errors(member, result.stdout))
|
||||
return errors
|
||||
|
||||
|
||||
@@ -123,12 +155,19 @@ def check_wheel(path: Path, *, inspect_dynamic: bool = True) -> None:
|
||||
raise ArtifactError(f"wheel does not exist: {path}")
|
||||
|
||||
with zipfile.ZipFile(path) as wheel:
|
||||
members = set(wheel.namelist())
|
||||
member_list = wheel.namelist()
|
||||
members = set(member_list)
|
||||
errors: list[str] = []
|
||||
|
||||
for member in members:
|
||||
pure = member.replace("\\", "/")
|
||||
if pure.startswith("/") or "../" in pure:
|
||||
if len(members) != len(member_list):
|
||||
errors.append("wheel contains duplicate member names")
|
||||
|
||||
for member in member_list:
|
||||
if "\\" in member or "\x00" in member:
|
||||
errors.append(f"unsafe wheel member: {member}")
|
||||
continue
|
||||
pure = PurePosixPath(member)
|
||||
if pure.is_absolute() or ".." in pure.parts:
|
||||
errors.append(f"unsafe wheel member: {member}")
|
||||
|
||||
for description, pattern in REQUIRED_MEMBERS:
|
||||
@@ -138,7 +177,36 @@ def check_wheel(path: Path, *, inspect_dynamic: bool = True) -> None:
|
||||
if not any(member.endswith(LAUNCHER_SUFFIX) for member in members):
|
||||
errors.append("missing packaged V100 release launcher")
|
||||
|
||||
native_members = sorted(member for member in members if member.endswith(".so"))
|
||||
wheel_metadata = [
|
||||
member for member in members if WHEEL_METADATA_PATTERN.fullmatch(member)
|
||||
]
|
||||
if not wheel_metadata:
|
||||
errors.append("missing wheel WHEEL metadata")
|
||||
else:
|
||||
tags = []
|
||||
for member in wheel_metadata:
|
||||
metadata = wheel.read(member).decode("utf-8", errors="replace")
|
||||
tags.extend(
|
||||
line.partition(":")[2].strip()
|
||||
for line in metadata.splitlines()
|
||||
if line.startswith("Tag:")
|
||||
)
|
||||
if not tags:
|
||||
errors.append("wheel WHEEL metadata has no Tag entry")
|
||||
elif not any(tag.startswith(f"{RELEASE_PYTHON_TAG}-") for tag in tags):
|
||||
errors.append(
|
||||
"SM70 release wheel must publish a Python 3.12-compatible Tag"
|
||||
)
|
||||
|
||||
# Do not inspect or extract native members until all member names have
|
||||
# passed validation. This keeps a malformed ZIP from writing outside
|
||||
# the temporary directory used by the dynamic dependency check.
|
||||
if errors:
|
||||
raise ArtifactError("; ".join(errors))
|
||||
|
||||
native_members = sorted(
|
||||
member for member in members if re.search(r"\.so(?:\..*)?$", member)
|
||||
)
|
||||
if inspect_dynamic:
|
||||
errors.extend(_check_dynamic_dependencies(wheel, native_members))
|
||||
|
||||
|
||||
Reference in new issue
Block a user