Files
ray-project--ray/release/ray_release/custom_byod_build_init_helper.py
2026-07-13 13:17:40 +08:00

265 lines
9.8 KiB
Python

import hashlib
import json
import os
import re
from typing import Dict, List, Optional, Set, Tuple
import yaml
from ray_release.configs.global_config import get_global_config
from ray_release.logger import logger
from ray_release.test import Test
# AZURE_REGISTRY_NAME import temporarily removed below: Azure image push is
# disabled due to CI issues. Re-add `AZURE_REGISTRY_NAME` to the import below
# when re-enabling the Azure ACR login step in create_custom_build_yaml.
from ray_release.util import ANYSCALE_RAY_IMAGE_PREFIX
def generate_custom_build_step_key(image: str) -> str:
image_repository, tag = image.split(":")
tag_variants = tag.split("-")
# Remove build id from the tag name to make hash consistent
image_name_without_id = f"{image_repository}:{'-'.join(tag_variants[1:])}"
logger.info(f"Image: {image_name_without_id}")
result = hashlib.sha256(image_name_without_id.encode()).hexdigest()[:20]
logger.info(f"Result: {result}")
return result
def get_images_from_tests(
tests: List[Test], build_id: str
) -> Tuple[
List[Tuple[str, str, Optional[str], Optional[str], Optional[Dict[str, str]]]],
Dict[str, List[str]],
]:
"""Get a list of custom BYOD images to build from a list of tests."""
custom_byod_images = {}
custom_image_test_names_map = {}
for test in tests:
if not test.require_custom_byod_image():
continue
image_tag = test.get_anyscale_byod_image(build_id)
if image_tag not in custom_byod_images:
runtime_env = test.get_byod_runtime_env() or None
custom_byod_images[image_tag] = (
image_tag,
test.get_anyscale_base_byod_image(build_id),
test.get_byod_post_build_script(),
test.get_byod_python_depset(),
runtime_env,
)
logger.info(f"To be built: {image_tag}")
if image_tag not in custom_image_test_names_map:
custom_image_test_names_map[image_tag] = []
custom_image_test_names_map[image_tag].append(test.get_name())
return list(custom_byod_images.values()), custom_image_test_names_map
def create_custom_build_yaml(
destination_file: str, tests: List[Test], gpu_map: Dict[str, str]
) -> None:
"""Create a yaml file for building custom BYOD images"""
config = get_global_config()
if (
not config
or not config.get("byod_ecr_region")
or not config.get("byod_ecr")
or not config.get("aws2gce_credentials")
):
raise ValueError(
"byod_ecr_region, byod_ecr, and aws2gce_credentials must be set in the config"
)
byod_ecr_region = config.get("byod_ecr_region")
byod_ecr = config.get("byod_ecr")
aws2gce_credentials = config.get("aws2gce_credentials")
custom_byod_images, custom_image_test_names_map = get_images_from_tests(
tests, "$$RAYCI_BUILD_ID"
)
if not custom_byod_images:
return
build_config = {"group": "Custom images build", "steps": []}
ray_want_commit = os.getenv("RAY_WANT_COMMIT_IN_IMAGE", "")
for (
image,
base_image,
post_build_script,
python_depset,
runtime_env,
) in custom_byod_images:
logger.info(
f"Building custom BYOD image: {image}, base image: {base_image}, "
f"post build script: {post_build_script}, runtime_env: {runtime_env}"
)
if not post_build_script and not python_depset and not runtime_env:
continue
step_key = generate_custom_build_step_key(image)
step_name = _get_step_name(image, step_key, custom_image_test_names_map[image])
env_args = ""
if runtime_env:
env_args = " ".join(
f"--env {k}={v}" for k, v in sorted(runtime_env.items())
)
build_cmd_parts = [
"bazelisk run //release:custom_byod_build --",
f"--image-name {image}",
f"--base-image {base_image}",
]
if post_build_script:
build_cmd_parts.append(f"--post-build-script {post_build_script}")
if python_depset:
build_cmd_parts.append(f"--python-depset {python_depset}")
if env_args:
build_cmd_parts.append(env_args)
build_cmd = " ".join(build_cmd_parts)
step = {
"label": step_name,
"key": step_key,
"instance_type": "release-medium",
"mount_buildkite_agent": True,
"commands": [
f"export RAY_WANT_COMMIT_IN_IMAGE={ray_want_commit}",
f"bash release/gcloud_docker_login.sh {aws2gce_credentials}",
"export PATH=$(pwd)/google-cloud-sdk/bin:$$PATH",
# Azure ACR login skipped: Azure image push is temporarily
# disabled due to CI issues.
"echo 'Skipping Azure ACR login: Azure image push is "
"temporarily disabled due to CI issues.'",
# "bash release/azure_docker_login.sh",
# f"az acr login --name {AZURE_REGISTRY_NAME}",
f"aws ecr get-login-password --region {byod_ecr_region} | docker login --username AWS --password-stdin {byod_ecr}",
build_cmd,
],
}
step["depends_on"] = get_prerequisite_step(image, base_image, gpu_map)
build_config["steps"].append(step)
with open(destination_file, "w") as f:
yaml.dump(build_config, f, default_flow_style=False, sort_keys=False)
def _short_tag(gpu: str) -> str:
"""Derive the short gpu tag from a full gpu string.
Examples: "cu12.3.2-cudnn9" → "cu123", "cpu" → "cpu".
"""
if gpu in ("cpu", "tpu"):
return gpu
base = gpu.split("-", 1)[0] # "cu12.3.2"
parts = base.split(".") # ["cu12", "3", "2"]
return f"{parts[0]}{parts[1]}" if len(parts) >= 2 else base
def build_short_gpu_map(ray_images_path: str) -> Dict[str, str]:
"""Build short-tag → full-gpu map from ray-images.json."""
if not os.path.exists(ray_images_path):
raise FileNotFoundError(f"ray-images.json not found at {ray_images_path}")
with open(ray_images_path) as f:
images = json.load(f)
gpus: Set[str] = set()
for cfg in images.values():
gpus.update(
cfg.get("platforms", [])
) # ray-images.json uses platforms instead of gpu.
result: Dict[str, str] = {}
for g in gpus:
short = _short_tag(g)
if short == "tpu":
continue # tpu is not used in release build steps
if short in result:
raise ValueError(
f"Collision detected for short tag '{short}': "
f"'{result[short]}' and '{g}' both map to the same tag."
)
result[short] = g
return result
def _sanitize_array_value(value: str) -> str:
"""Strip non-alphanumeric characters, matching rayci's array key logic."""
return re.sub(r"[^a-zA-Z0-9]", "", value)
def get_prerequisite_step(
image: str,
base_image: str,
gpu_map: Optional[Dict[str, str]] = None,
) -> Optional[str]:
"""Get the base image build step for a job that depends on it.
Returns the array-suffixed Buildkite step key for the publish step
that produces the base image.
"""
if gpu_map is None:
gpu_map = {}
config = get_global_config()
image_repository, _ = image.split(":")
image_name = image_repository.split("/")[-1]
if base_image.startswith(ANYSCALE_RAY_IMAGE_PREFIX):
return "forge"
# Parse base_image tag: {build_id}-py{ver}-{suffix}
_, tag = base_image.rsplit(":", 1)
tag_parts = tag.split("-")
py_index = None
for i, part in enumerate(tag_parts):
if re.match(r"^py\d+$", part):
py_index = i
break
if image_name == "ray-ml":
bare_key = config["release_image_step_ray_ml"]
if py_index is None:
return bare_key
return f"{bare_key}--python{tag_parts[py_index][2:]}"
if image_name == "ray-llm":
bare_key = config["release_image_step_ray_llm"]
else:
# ray: pick cpu or cuda key based on tag suffix
tag_suffix = "-".join(tag_parts[py_index + 1 :]) if py_index is not None else ""
if tag_suffix == "cpu":
bare_key = config["release_image_step_ray_cpu"]
else:
bare_key = config["release_image_step_ray_cuda"]
if py_index is None:
return bare_key
python_raw = tag_parts[py_index][2:] # e.g., "310"
tag_suffix = "-".join(tag_parts[py_index + 1 :]) # e.g., "cu123" or "cpu"
if tag_suffix == "cpu":
# anyscalecpubuild has only a python dimension
return f"{bare_key}--python{python_raw}"
# cuda publish steps have gpu + python dimensions (alphabetical order)
full_gpu = gpu_map.get(tag_suffix, tag_suffix)
sanitized = _sanitize_array_value(full_gpu)
return f"{bare_key}--gpu{sanitized}-python{python_raw}"
def collect_rayci_select_keys(tests: List[Test], gpu_map: Dict[str, str]) -> Set[str]:
"""Return the RAYCI_SELECT key set (base-image + custom-BYOD) for the given tests."""
keys: Set[str] = set()
for test in tests:
image = test.get_anyscale_byod_image()
base_image = test.get_anyscale_base_byod_image()
prereq = get_prerequisite_step(image, base_image, gpu_map)
if prereq:
keys.add(prereq)
if test.require_custom_byod_image():
keys.add(generate_custom_build_step_key(image))
return keys
def _get_step_name(image: str, step_key: str, test_names: List[str]) -> str:
ecr, tag = image.split(":")
ecr_repo = ecr.split("/")[-1]
tag_without_build_id_and_custom_hash = tag.split("-")[1:-1]
step_name = f":tapioca: build custom: {ecr_repo}:{'-'.join(tag_without_build_id_and_custom_hash)} ({step_key})"
for test_name in test_names[:2]:
step_name += f" {test_name}"
return step_name