Skip to content

Commit ebe6f44

Browse files
authored
refactor support policy grouping (#5655)
1 parent 6672eaa commit ebe6f44

File tree

6 files changed

+222
-197
lines changed

6 files changed

+222
-197
lines changed

.kiro/steering/docs.md

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ docs/src/
7676

7777
- `constants.py` - Path constants, global variables, `GLOBAL_CONFIG`, `SITE_URL`, `README_PATH`, and `RELEASE_NOTES_REQUIRED_FIELDS`
7878
- `sorter.py` - Sorting tiebreaker functions: `platform_sorter`, `accelerator_sorter`, `repository_sorter`
79-
- `utils.py` - Utility functions: `load_yaml()`, `load_table_config()`, `load_jinja2()`, `render_table()`, `write_output()`, `parse_version()`, `clone_git_repository()`, `build_ecr_uri()`, `build_public_ecr_uri()`, `get_framework_order()`
79+
- `utils.py` - Utility functions: `load_yaml()`, `load_table_config()`, `load_jinja2()`, `render_table()`, `write_output()`, `parse_version()`, `clone_git_repository()`, `build_ecr_uri()`, `build_public_ecr_uri()`, `get_framework_order()`, `flatten_group_repos()`
8080
- `image_config.py` - `ImageConfig` class, image loaders (`load_repository_images`, `load_legacy_images`, `load_images_by_framework_group`), `sort_by_version`, `get_latest_image_uri`, `build_image_row`, `check_public_registry`
81-
- `generate.py` - `generate_index()`, `generate_support_policy()`, `generate_available_images()`, `generate_release_notes()`, `generate_all()`
81+
- `generate.py` - `generate_index()`, `generate_support_policy()`, `generate_available_images()`, `generate_release_notes()`, `generate_all()`, helpers: `_consolidate_framework_version()`, `_collapse_minor_versions()`
8282
- `macros.py` - MkDocs macros plugin integration
8383
- `hooks.py` - MkDocs hooks entry point
8484

@@ -356,16 +356,19 @@ display_names:
356356
known_issues: "Known Issues"
357357

358358
# Framework groups for support policy consolidation (lowercase keys)
359+
# Flat list: repos split directly to per-repo rows on mismatch
360+
# Nested dict: repos consolidate by sub-group first on mismatch
359361
framework_groups:
360362
pytorch:
361-
- pytorch-training
362-
- pytorch-inference
363-
- pytorch-training-arm64
364-
- pytorch-inference-arm64
365-
tensorflow:
366-
- tensorflow-training
367-
- tensorflow-inference
368-
- tensorflow-inference-arm64
363+
pytorch-training:
364+
- pytorch-training
365+
- pytorch-training-arm64
366+
pytorch-inference:
367+
- pytorch-inference
368+
- pytorch-inference-arm64
369+
vllm:
370+
- vllm
371+
- vllm-arm64
369372

370373
# Table order (controls order in available_images.md and support_policy.md)
371374
table_order:
@@ -376,30 +379,25 @@ table_order:
376379

377380
### Support Policy Consolidation
378381

379-
The `framework_groups` configuration consolidates support policy rows by framework. Repositories in the same group are combined into a single row using the framework name (e.g., "PyTorch") when they share the same GA/EOP dates.
382+
The `framework_groups` configuration consolidates support policy rows by framework. The generation follows a 3-step flow:
380383

381-
**Version Display:**
384+
**Validation (at load time):** `load_repository_images` validates that images sharing the same full version within a single repository have identical GA/EOP dates. Raises `ValueError` if not (this is a data bug).
382385

383-
- Images with the same major.minor version and identical GA/EOP dates are consolidated into a single row displayed as `2.6` with the framework group name (e.g., "PyTorch")
384-
- If the same version has different GA/EOP dates across repository types (e.g., training vs inference), separate rows are created showing the specific repository type: "PyTorch Training" and "PyTorch Inference"
385-
- ARM64 variants are automatically consolidated with their base repository (e.g., "PyTorch Training" and "PyTorch Training ARM64" both show as "PyTorch Training")
386-
- If patch versions within the same repository have different GA/EOP dates, each is displayed separately with full version (e.g., `2.6.0`, `2.6.1`) and a warning is logged
386+
**Step 1 — Group by full version:** All images in a framework group are grouped by full version (e.g., `2.6.0`), deduplicated per repository (one representative image per repo since intra-repo consistency is guaranteed).
387387

388-
**Behavior:**
388+
**Step 2 — Hierarchical consolidation** via `_consolidate_framework_version()`: For each full version, tries three levels of date agreement, stopping at the first that succeeds:
389389

390-
- Repositories in the same framework group can have different GA/EOP dates for the same version (e.g., inference can have a different EOP than training)
391-
- When dates match across all repositories in a group, they are consolidated into a single row with the framework group name
392-
- When dates differ by repository type (training vs inference), separate rows show the specific repository type names
393-
- Missing versions in some repositories are allowed (only present repos are included)
390+
1. Framework group — all repos agree → single row (e.g., "PyTorch")
391+
1. Sub-group — repos within a sub-group agree → one row per sub-group (e.g., "PyTorch Inference"). Only applies to nested dict groups in `framework_groups`.
392+
1. Per-repo — no agreement → one row per repository using its individual display name
394393

395-
**Example:**
394+
**Step 3 — Major.minor collapse:** Non-split entries are grouped by major.minor. If all full versions within a major.minor share the same dates, they collapse into a single row displayed as the major.minor (e.g., `2.6`). Collapse is skipped for any major.minor that has split (per-repo) rows.
396395

397-
If PyTorch 2.6 Training has EOP 2025-10-15 but PyTorch 2.6 Inference has EOP 2026-10-15, the support policy table will show:
396+
**Behavior:**
398397

399-
- Row 1: Framework="PyTorch Training", Version="2.6", EOP="2025-10-15"
400-
- Row 2: Framework="PyTorch Inference", Version="2.6", EOP="2026-10-15"
398+
- Missing versions in some repositories are allowed (only present repos are consolidated)
401399

402-
To add a new framework group, add an entry to `framework_groups` with the framework name as key and list of repositories as value.
400+
To add a new framework group, add an entry to `framework_groups` with the framework name as key and list of repositories as value. Use nested dict format if you need intermediate sub-group consolidation when dates differ.
403401

404402
### Reordering Tables and Columns
405403

docs/src/generate.py

Lines changed: 122 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
1+
## Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License"). You
44
# may not use this file except in compliance with the License. A copy of
@@ -32,13 +32,15 @@
3232
ImageConfig,
3333
build_image_row,
3434
check_public_registry,
35+
dates_agree,
3536
load_images_by_framework_group,
3637
load_legacy_images,
3738
load_repository_images,
3839
sort_by_version,
3940
)
4041
from jinja2 import Template
4142
from utils import (
43+
build_repo_map,
4244
get_framework_order,
4345
load_jinja2,
4446
load_table_config,
@@ -176,6 +178,102 @@ def generate_release_notes(dry_run: bool = False) -> None:
176178
LOGGER.info("Generated release notes")
177179

178180

181+
def _consolidate_framework_version(
182+
framework_group: str,
183+
full_ver: str,
184+
repo_imgs: list[ImageConfig],
185+
) -> list[tuple[ImageConfig, dict[str, str]]]:
186+
"""Consolidate images for a single framework version using hierarchical date agreement.
187+
188+
Tries three levels of consolidation, stopping at the first that succeeds:
189+
1. Framework group — all repos agree → single row
190+
2. Sub-group — repos within a sub-group agree → one row per sub-group (nested groups only)
191+
3. Per-repo — no agreement → one row per repository
192+
"""
193+
# All repos agree → single framework-level row (Level 1 consolidation)
194+
if dates_agree(repo_imgs):
195+
return [(repo_imgs[0], {"version": full_ver})]
196+
197+
LOGGER.warning(
198+
f"GA/EOP mismatch in {framework_group} {full_ver} across repositories. "
199+
f"Splitting into sub-group/repository rows."
200+
)
201+
202+
group_config = GLOBAL_CONFIG.get("framework_groups", {}).get(framework_group, [])
203+
204+
# Flat group — no sub-groups, fall back directly to per-repo rows (Level 3 consolidation)
205+
if not isinstance(group_config, dict):
206+
return [
207+
(img, {"version": full_ver, "framework_group": img.display_repository})
208+
for img in repo_imgs
209+
]
210+
211+
# Nested group — try sub-group consolidation (Level 2 consolidation), per-repo fallback (Level 3 consolidation)
212+
repo_to_subgroup = build_repo_map(group_config)
213+
subgroup_imgs: dict[str, list[ImageConfig]] = {}
214+
for img in repo_imgs:
215+
subgroup_name = repo_to_subgroup.get(img._repository, img._repository)
216+
subgroup_imgs.setdefault(subgroup_name, []).append(img)
217+
218+
entries: list[tuple[ImageConfig, dict[str, str]]] = []
219+
for subgroup_name, images in subgroup_imgs.items():
220+
if dates_agree(images):
221+
display_name = GLOBAL_CONFIG.get("display_names", {}).get(subgroup_name, subgroup_name)
222+
entries.append((images[0], {"version": full_ver, "framework_group": display_name}))
223+
else:
224+
entries.extend(
225+
(img, {"version": full_ver, "framework_group": img.display_repository})
226+
for img in images
227+
)
228+
229+
return entries
230+
231+
232+
def _collapse_minor_versions(
233+
entries: list[tuple[ImageConfig, dict[str, str]]],
234+
) -> list[tuple[ImageConfig, dict[str, str]]]:
235+
"""Collapse patch versions (e.g., A.B.C, A.B.D) into major.minor (A.B) when all share identical dates.
236+
237+
Skips any major.minor that has split (per-repo) rows, since mixing collapsed and split rows
238+
under the same major.minor would be confusing.
239+
240+
Args:
241+
entries: List of (image, overrides) tuples. Split rows have "framework_group" in overrides.
242+
243+
Returns:
244+
New list with collapsible groups replaced by a single major.minor entry.
245+
"""
246+
uncollapsible: set[str] = set()
247+
collapsible_groups: dict[str, list[int]] = {}
248+
for idx, (img, overrides) in enumerate(entries):
249+
version_obj = parse_version(img.version)
250+
mm = f"{version_obj.major}.{version_obj.minor}"
251+
if "framework_group" in overrides:
252+
# Find major.minors that have split rows by repository — these cannot be collapsed
253+
# Collapsing split rows will create ambiguity between patch versions
254+
uncollapsible.add(mm)
255+
else:
256+
collapsible_groups.setdefault(mm, []).append(idx)
257+
258+
# Collapse: if all entries in a major.minor group share dates, keep one with major.minor display
259+
for mm, indices in collapsible_groups.items():
260+
if mm in uncollapsible:
261+
continue
262+
group_imgs = [entries[idx][0] for idx in indices]
263+
ref_img = group_imgs[0]
264+
if dates_agree(group_imgs):
265+
entries[indices[0]] = (ref_img, {"version": mm})
266+
for idx in indices[1:]:
267+
entries[idx] = None # mark duplicates for removal
268+
else:
269+
LOGGER.warning(
270+
f"Cannot collapse {ref_img._repository} {mm}. "
271+
f"Please confirm images GA/EOP dates within this framework are intentional."
272+
)
273+
274+
return [e for e in entries if e is not None]
275+
276+
179277
def generate_support_policy(dry_run: bool = False) -> str:
180278
"""Generate support_policy.md from image configs with GA/EOP dates."""
181279
output_path = REFERENCE_DIR / "support_policy.md"
@@ -197,156 +295,41 @@ def generate_support_policy(dry_run: bool = False) -> str:
197295
if not images:
198296
continue
199297

200-
# Group by (major.minor, ga, eop) to allow different dates for same version
201-
# This enables training and inference to have different EOP dates
202-
date_groups: dict[tuple[str, str, str], list[ImageConfig]] = {}
298+
# Step 1: Group by full version, deduplicate per repo
299+
version_entries: dict[str, list[ImageConfig]] = {}
203300
for img in images:
204-
v = parse_version(img.version)
205-
major_minor = f"{v.major}.{v.minor}"
206-
key = (major_minor, img.ga, img.eop)
207-
date_groups.setdefault(key, []).append(img)
208-
209-
# Track which versions have multiple date groups (need repository-specific display)
210-
version_date_count: dict[str, int] = {}
211-
for (major_minor, ga, eop), group in date_groups.items():
212-
version_date_count[major_minor] = version_date_count.get(major_minor, 0) + 1
213-
214-
version_map: dict[str, tuple[ImageConfig, bool]] = {}
215-
216-
# Process each unique (version, ga, eop) combination
217-
for (major_minor, ga, eop), group in date_groups.items():
218-
# Check if all images in this date group have the same full version
219-
versions_in_group = {img.version for img in group}
220-
221-
# Determine if this version needs repository-specific display
222-
needs_repo_display = version_date_count[major_minor] > 1
223-
224-
if len(versions_in_group) == 1:
225-
# All images have same patch version
226-
first = group[0]
227-
228-
if needs_repo_display:
229-
# Same version exists with different dates - use repository-specific display
230-
# Store with flag indicating we need to override framework_group display
231-
repos_in_group = {img._repository for img in group}
232-
# Create a unique key for this date group
233-
repo_suffix = "-".join(sorted(repos_in_group))
234-
display_key = f"{major_minor}:{repo_suffix}"
235-
version_map[display_key] = (first, True) # True = use repo display
236-
else:
237-
# No conflict - use simple major.minor key with framework display
238-
version_map[major_minor] = (first, False) # False = use framework display
239-
else:
240-
# Multiple patch versions with same dates - warn and keep separate
241-
versions_info = ", ".join(sorted(versions_in_group))
242-
LOGGER.warning(
243-
f"Different patch versions for {framework_group} with same GA/EOP dates: {versions_info}"
244-
)
245-
for img in group:
246-
version_map[img.version] = (img, needs_repo_display)
301+
bucket = version_entries.setdefault(img.version, [])
302+
if not any(existing._repository == img._repository for existing in bucket):
303+
bucket.append(img)
304+
305+
# Step 2: Consolidate across repos (framework → sub-group → per-repo fallback)
306+
entries: list[tuple[ImageConfig, dict[str, str]]] = []
307+
for full_ver, repo_imgs in version_entries.items():
308+
entries.extend(_consolidate_framework_version(framework_group, full_ver, repo_imgs))
309+
310+
# Step 3: Collapse patch versions into major.minor where possible
311+
entries = _collapse_minor_versions(entries)
247312

248313
# Merge legacy entries for this framework
249314
for legacy_img in legacy_data.get(framework_group, []):
250-
if legacy_img.version not in version_map:
251-
version_map[legacy_img.version] = (
252-
legacy_img,
253-
False,
254-
) # Legacy uses framework display
315+
entries.append((legacy_img, {"version": legacy_img.version}))
255316

256317
# Sort by version descending within this framework group
257-
# Extract version for sorting from the tuple
258-
sorted_keys = sorted(
259-
version_map.keys(), key=lambda k: parse_version(version_map[k][0].version), reverse=True
260-
)
261-
for key in sorted_keys:
262-
img, use_repo_display = version_map[key]
263-
# Extract clean version for display (remove repo suffix if present)
264-
display_version = key.split(":")[0] if ":" in key else key
265-
(supported if img.is_supported else unsupported).append(
266-
(img, display_version, use_repo_display)
267-
)
318+
entries.sort(key=lambda e: parse_version(e[0].version), reverse=True)
319+
for img, overrides in entries:
320+
(supported if img.is_supported else unsupported).append((img, overrides))
268321

269322
# Build tables
270323
table_config = load_table_config("extra/support_policy")
271324
columns = table_config.get("columns", [])
272325
headers = [col["header"] for col in columns]
273326

274-
# Build rows with appropriate framework display
275-
supported_rows = []
276-
for img, ver, use_repo_display in supported:
277-
overrides = {"version": ver}
278-
if use_repo_display:
279-
# Find all repositories in this framework group with this version and same dates
280-
# to create a comprehensive display name
281-
all_repos_with_dates = [
282-
i
283-
for i in images_by_group.get(img.framework_group, [])
284-
if parse_version(i.version).major == parse_version(img.version).major
285-
and parse_version(i.version).minor == parse_version(img.version).minor
286-
and i.ga == img.ga
287-
and i.eop == img.eop
288-
]
289-
unique_repos = sorted(set(i._repository for i in all_repos_with_dates))
290-
display_names = GLOBAL_CONFIG.get("display_names", {})
291-
292-
# Determine the common prefix (e.g., "PyTorch") and suffix (e.g., "Training", "Inference")
293-
repo_displays = [display_names.get(repo, repo) for repo in unique_repos]
294-
295-
# If all repos share a common framework prefix, consolidate intelligently
296-
# e.g., ["PyTorch Training", "PyTorch Training ARM64"] -> "PyTorch Training"
297-
# e.g., ["PyTorch Inference", "PyTorch Inference ARM64"] -> "PyTorch Inference"
298-
if len(repo_displays) > 1:
299-
# Check if we can consolidate (e.g., remove ARM64 variants)
300-
base_displays = set()
301-
for display in repo_displays:
302-
# Remove " ARM64" suffix if present
303-
base = display.replace(" ARM64", "").strip()
304-
base_displays.add(base)
305-
306-
if len(base_displays) == 1:
307-
# All are variants of the same base (e.g., all "PyTorch Training")
308-
overrides["framework_group"] = base_displays.pop()
309-
else:
310-
# Multiple different bases - show them all
311-
overrides["framework_group"] = ", ".join(sorted(base_displays))
312-
else:
313-
overrides["framework_group"] = repo_displays[0]
314-
supported_rows.append(build_image_row(img, columns, overrides))
315-
316-
unsupported_rows = []
317-
for img, ver, use_repo_display in unsupported:
318-
overrides = {"version": ver}
319-
if use_repo_display:
320-
# Find all repositories in this framework group with this version and same dates
321-
all_repos_with_dates = [
322-
i
323-
for i in images_by_group.get(img.framework_group, [])
324-
if parse_version(i.version).major == parse_version(img.version).major
325-
and parse_version(i.version).minor == parse_version(img.version).minor
326-
and i.ga == img.ga
327-
and i.eop == img.eop
328-
]
329-
unique_repos = sorted(set(i._repository for i in all_repos_with_dates))
330-
display_names = GLOBAL_CONFIG.get("display_names", {})
331-
332-
repo_displays = [display_names.get(repo, repo) for repo in unique_repos]
333-
334-
if len(repo_displays) > 1:
335-
base_displays = set()
336-
for display in repo_displays:
337-
base = display.replace(" ARM64", "").strip()
338-
base_displays.add(base)
339-
340-
if len(base_displays) == 1:
341-
overrides["framework_group"] = base_displays.pop()
342-
else:
343-
overrides["framework_group"] = ", ".join(sorted(base_displays))
344-
else:
345-
overrides["framework_group"] = repo_displays[0]
346-
unsupported_rows.append(build_image_row(img, columns, overrides))
347-
348-
supported_table = render_table(headers, supported_rows)
349-
unsupported_table = render_table(headers, unsupported_rows)
327+
supported_table = render_table(
328+
headers, [build_image_row(img, columns, overrides) for img, overrides in supported]
329+
)
330+
unsupported_table = render_table(
331+
headers, [build_image_row(img, columns, overrides) for img, overrides in unsupported]
332+
)
350333

351334
# Render template
352335
template = Template(load_jinja2(template_path))

0 commit comments

Comments
 (0)