Skip to content

Commit 06274d1

Browse files
[CI] Add filter_release_build_yaml to trim build steps by test needs
Add collect_needed_variants() to extract python versions, image types, and CUDA platforms from selected tests. Add filter_release_build_yaml() to rewrite build.rayci.yml in-place, removing steps for unneeded image types and trimming array dimensions (python, platform, cuda) and adjustments to only needed values. Steps left with no viable combinations are removed entirely. This is the library/logic layer — the init script integration comes in the next commit. Topic: release-build-filter Relative: array-release Labels: draft Signed-off-by: andrew <[email protected]>
1 parent b3d5a18 commit 06274d1

2 files changed

Lines changed: 467 additions & 1 deletion

File tree

release/ray_release/custom_byod_build_init_helper.py

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import hashlib
22
import os
33
import re
4-
from typing import Dict, List, Optional, Tuple
4+
from typing import Dict, List, Optional, Set, Tuple
55

66
import yaml
77

@@ -215,3 +215,173 @@ def _get_step_name(image: str, step_key: str, test_names: List[str]) -> str:
215215
for test_name in test_names[:2]:
216216
step_name += f" {test_name}"
217217
return step_name
218+
219+
220+
def collect_needed_variants(
221+
tests: List[Test],
222+
) -> Tuple[Set[str], Set[str], Dict[str, Optional[Set[str]]]]:
223+
"""Collect needed build variants from selected tests.
224+
225+
Returns:
226+
needed_python: Python version strings (e.g., {"3.10", "3.12"}).
227+
needed_image_types: Image type categories:
228+
"ray-cpu", "ray-cuda", "ray-ml", "ray-llm".
229+
cuda_needs: Per-image-type CUDA platform needs. A set of full
230+
platform strings (e.g., {"cu12.3.2-cudnn9"}) means only those
231+
platforms are needed. ``None`` means keep all CUDA variants
232+
(used when the test's byod_type doesn't map to a specific
233+
platform, e.g. "gpu" for ray-ml).
234+
"""
235+
needed_python: Set[str] = set()
236+
needed_image_types: Set[str] = set()
237+
cuda_needs: Dict[str, Optional[Set[str]]] = {}
238+
239+
for test in tests:
240+
needed_python.add(test.get_python_version())
241+
tag_suffix = test.get_tag_suffix()
242+
243+
if test.use_byod_ml_image():
244+
img_type = "ray-ml"
245+
elif test.use_byod_llm_image():
246+
img_type = "ray-llm"
247+
elif tag_suffix == "cpu":
248+
needed_image_types.add("ray-cpu")
249+
continue
250+
else:
251+
img_type = "ray-cuda"
252+
253+
needed_image_types.add(img_type)
254+
platform = _SHORT_PLATFORM_MAP.get(tag_suffix)
255+
256+
if platform:
257+
if img_type not in cuda_needs or cuda_needs[img_type] is not None:
258+
cuda_needs.setdefault(img_type, set()).add(platform)
259+
else:
260+
# Can't determine specific CUDA platform; keep all variants.
261+
cuda_needs[img_type] = None
262+
263+
return needed_python, needed_image_types, cuda_needs
264+
265+
266+
def _get_step_image_type(step: dict) -> Optional[str]:
267+
"""Determine the image type category of a build step.
268+
269+
Returns one of "ray-cpu", "ray-cuda", "ray-ml", "ray-llm", or None.
270+
"""
271+
name = step.get("name", "")
272+
key = step.get("key", "")
273+
image_type = step.get("env", {}).get("IMAGE_TYPE", "")
274+
275+
if image_type == "ray-llm" or "llm" in name or "llm" in key:
276+
return "ray-llm"
277+
if image_type == "ray-ml" or "ray-ml" in name or "ml" in key:
278+
return "ray-ml"
279+
if "cpu" in name or "cpu" in key:
280+
return "ray-cpu"
281+
if "cuda" in name or "cuda" in key:
282+
return "ray-cuda"
283+
return None
284+
285+
286+
def _filter_array_dimension(
287+
values: List[str],
288+
allowed: Set[str],
289+
prefix: str = "",
290+
) -> List[str]:
291+
"""Filter an array dimension to only allowed values.
292+
293+
``prefix`` is prepended to each allowed value before comparison.
294+
For cuda arrays whose values lack the "cu" prefix, pass prefix="cu".
295+
"""
296+
return [v for v in values if f"{prefix}{v}" in allowed]
297+
298+
299+
def filter_release_build_yaml(
300+
path: str,
301+
needed_python: Set[str],
302+
needed_image_types: Set[str],
303+
cuda_needs: Dict[str, Optional[Set[str]]],
304+
) -> None:
305+
"""Filter build.rayci.yml to only include needed variants.
306+
307+
Modifies the file in-place. Steps for unneeded image types are removed.
308+
Array dimensions (python, platform, cuda) and adjustments are trimmed to
309+
the needed values. Steps left with no viable combinations are removed.
310+
"""
311+
with open(path) as f:
312+
data = yaml.safe_load(f)
313+
314+
filtered_steps = []
315+
for step in data.get("steps", []):
316+
img_type = _get_step_image_type(step)
317+
if img_type is not None and img_type not in needed_image_types:
318+
continue
319+
320+
array = step.get("array")
321+
if not array:
322+
filtered_steps.append(step)
323+
continue
324+
325+
# Filter python dimension.
326+
if "python" in array:
327+
array["python"] = [v for v in array["python"] if v in needed_python]
328+
329+
# Determine which CUDA platforms to keep for this step's image type.
330+
cuda_filter = cuda_needs.get(img_type) if img_type else None
331+
should_filter_cuda = img_type in cuda_needs and cuda_filter is not None
332+
333+
if should_filter_cuda:
334+
if "platform" in array:
335+
array["platform"] = [v for v in array["platform"] if v in cuda_filter]
336+
if "cuda" in array:
337+
# cuda array values don't have the "cu" prefix.
338+
cuda_no_prefix = {p.removeprefix("cu") for p in cuda_filter}
339+
array["cuda"] = [v for v in array["cuda"] if v in cuda_no_prefix]
340+
341+
# Filter adjustments.
342+
if "adjustments" in array:
343+
filtered_adj = []
344+
for adj in array["adjustments"]:
345+
w = adj.get("with", {})
346+
keep = True
347+
if "python" in w and w["python"] not in needed_python:
348+
keep = False
349+
if should_filter_cuda:
350+
if "platform" in w and w["platform"] not in cuda_filter:
351+
keep = False
352+
if "cuda" in w:
353+
cuda_no_prefix = {p.removeprefix("cu") for p in cuda_filter}
354+
if w["cuda"] not in cuda_no_prefix:
355+
keep = False
356+
if keep:
357+
filtered_adj.append(adj)
358+
if filtered_adj:
359+
array["adjustments"] = filtered_adj
360+
elif "adjustments" in array:
361+
del array["adjustments"]
362+
363+
# If base arrays were emptied but adjustments remain, reconstruct
364+
# minimal base arrays from adjustment values so rayci can expand them.
365+
remaining_adj = array.get("adjustments", [])
366+
for dim in list(array.keys()):
367+
if dim == "adjustments" or not isinstance(array[dim], list):
368+
continue
369+
if len(array[dim]) == 0 and remaining_adj:
370+
values = sorted(
371+
{a["with"][dim] for a in remaining_adj if dim in a.get("with", {})}
372+
)
373+
array[dim] = values
374+
375+
# Check if any combinations remain.
376+
base_dims = [
377+
v for k, v in array.items() if k != "adjustments" and isinstance(v, list)
378+
]
379+
has_base_combos = all(len(d) > 0 for d in base_dims) if base_dims else False
380+
has_adjustments = bool(array.get("adjustments"))
381+
382+
if has_base_combos or has_adjustments:
383+
filtered_steps.append(step)
384+
385+
data["steps"] = filtered_steps
386+
with open(path, "w") as f:
387+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)

0 commit comments

Comments
 (0)