CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
519 lines
17 KiB
Python
519 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright © Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
"""
|
|
Place captured graph bundles into the template+sweep format (ALMIOPEN-2221, AC #8).
|
|
|
|
Hop B of the migration pipeline:
|
|
C++ graph test --(Hop A: --capture-bundles)--> standalone bundle
|
|
--(Hop B: this script)--------> template+sweep
|
|
|
|
Reads the flat per-case output of ``--capture-bundles`` and groups graphs by
|
|
STRUCTURE (topology), collapsing cases that share the same graph skeleton and
|
|
differ only in per-case data (dims/strides/dtype/inline values/node attributes)
|
|
into one ``graph.template.json`` + ``sweep.json``.
|
|
|
|
Conforms to the Compressed Template Sweeps spec:
|
|
integration_test_bundles/{Tier}/{Operation}/{TopologyName}/
|
|
graph.template.json
|
|
sweep.json
|
|
|
|
Design principle -- derive, don't classify. "Structural" is defined narrowly and
|
|
mechanically (node types + wiring + tensor set); everything else is a per-case
|
|
knob. Round-trip verification is the correctness proof: any bad merge fails
|
|
loudly and falls back to a standalone single-graph bundle. Nothing is dropped.
|
|
|
|
Usage::
|
|
|
|
place_bundles.py --capture-dir <path> --output-dir <path> [--dry-run] [--no-verify]
|
|
"""
|
|
|
|
import argparse
|
|
import copy
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from bundle_utils import (
|
|
DTYPE_TOKEN,
|
|
NODE_STRUCTURAL,
|
|
TENSOR_ALWAYS,
|
|
TENSOR_IF_VARIES,
|
|
TIER_MAP,
|
|
TOP_LEVEL_IF_VARIES,
|
|
ambiguous_attr_keys,
|
|
assign_case_ids,
|
|
canon,
|
|
canonical_uid_map_by_name,
|
|
derive_operation,
|
|
expand,
|
|
infer_layout,
|
|
node_attr_items,
|
|
raw_node_attrs,
|
|
remap_graph,
|
|
remap_meta_inputs,
|
|
sanitize,
|
|
skeleton_hash,
|
|
tensors_by_uid,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class CapturedCase:
|
|
suite: str
|
|
case_name: str
|
|
graph_path: Path
|
|
meta_path: Path
|
|
graph: dict
|
|
meta: dict
|
|
tier: str = "quick"
|
|
original_graph: dict = None
|
|
|
|
|
|
@dataclass
|
|
class Bucket:
|
|
"""A set of captured cases sharing one graph skeleton."""
|
|
|
|
skeleton_hash: str
|
|
tier: str
|
|
operation: str
|
|
cases: list = field(default_factory=list)
|
|
topology_name: str = ""
|
|
|
|
|
|
@dataclass
|
|
class Stats:
|
|
cases_found: int = 0
|
|
buckets: int = 0
|
|
sweeps_written: int = 0
|
|
sweep_cases: int = 0
|
|
standalone_written: int = 0
|
|
verify_pass: int = 0
|
|
verify_fail: int = 0
|
|
errors: list = field(default_factory=list)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 1. Discovery
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def discover_captures(capture_dir: Path) -> list[CapturedCase]:
|
|
"""Walk capture directory and load all captured cases.
|
|
|
|
C++ capture writes <capture-dir>/<suiteName>/<safeCaseName>/<safeCaseName>.json
|
|
where suiteName may contain '/' (e.g. 'Smoke/IntegrationGpuConvFp32'). We find
|
|
cases by locating .json files (excluding .meta.json) whose stem matches the
|
|
parent directory name.
|
|
"""
|
|
cases = []
|
|
if not capture_dir.is_dir():
|
|
print(
|
|
f"place_bundles: capture dir does not exist: {capture_dir}", file=sys.stderr
|
|
)
|
|
return cases
|
|
|
|
for graph_path in sorted(capture_dir.rglob("*.json")):
|
|
if graph_path.name.endswith(".meta.json"):
|
|
continue
|
|
|
|
case_dir = graph_path.parent
|
|
case_name = case_dir.name
|
|
if graph_path.stem != case_name:
|
|
continue
|
|
|
|
suite_rel = case_dir.parent.relative_to(capture_dir)
|
|
suite_name = str(suite_rel)
|
|
|
|
try:
|
|
with open(graph_path) as f:
|
|
graph = json.load(f)
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f" WARN: bad graph {graph_path}: {e}", file=sys.stderr)
|
|
continue
|
|
|
|
meta_path = case_dir / f"{case_name}.meta.json"
|
|
meta = {"format_version": 1}
|
|
if meta_path.exists():
|
|
try:
|
|
with open(meta_path) as f:
|
|
meta = json.load(f)
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f" WARN: bad meta {meta_path}: {e}", file=sys.stderr)
|
|
|
|
tier = TIER_MAP.get(suite_rel.parts[0], "quick") if suite_rel.parts else "quick"
|
|
|
|
cases.append(
|
|
CapturedCase(
|
|
suite=suite_name,
|
|
case_name=case_name,
|
|
graph_path=graph_path,
|
|
meta_path=meta_path,
|
|
graph=graph,
|
|
meta=meta,
|
|
tier=tier,
|
|
)
|
|
)
|
|
return cases
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 2. Knob detection + template / sweep construction
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def detect_and_build(bucket: Bucket):
|
|
"""Analyze a bucket, returning (template, sweep_cases, error)."""
|
|
cases = bucket.cases
|
|
rep = cases[0].graph
|
|
ambiguous = ambiguous_attr_keys(rep)
|
|
|
|
top_varies = set()
|
|
for fld in TOP_LEVEL_IF_VARIES:
|
|
vals = {json.dumps(c.graph.get(fld)) for c in cases}
|
|
if len(vals) > 1:
|
|
top_varies.add(fld)
|
|
|
|
rep_tensors = tensors_by_uid(rep)
|
|
tensor_value_varies = {}
|
|
for uid in rep_tensors:
|
|
varies = set()
|
|
for fld in TENSOR_IF_VARIES:
|
|
vals = {
|
|
json.dumps(tensors_by_uid(c.graph).get(uid, {}).get(fld)) for c in cases
|
|
}
|
|
if len(vals) > 1:
|
|
varies.add(fld)
|
|
tensor_value_varies[uid] = varies
|
|
|
|
attr_varies = set()
|
|
for ni in range(len(rep.get("nodes", []))):
|
|
for attr_key, _ in node_attr_items(rep["nodes"][ni], ni, ambiguous):
|
|
vals = set()
|
|
for c in cases:
|
|
nodes = c.graph.get("nodes", [])
|
|
if ni < len(nodes):
|
|
d = dict(node_attr_items(nodes[ni], ni, ambiguous))
|
|
vals.add(json.dumps(d.get(attr_key)))
|
|
if len(vals) > 1:
|
|
attr_varies.add(attr_key)
|
|
|
|
template = copy.deepcopy(rep)
|
|
for fld in top_varies:
|
|
template[fld] = f"${{case.{fld}}}"
|
|
for t in template.get("tensors", []):
|
|
uid = t.get("uid")
|
|
for fld in TENSOR_ALWAYS:
|
|
if fld in t:
|
|
t[fld] = f"${{case.{fld}}}"
|
|
for fld in tensor_value_varies.get(uid, set()):
|
|
if fld in t:
|
|
t[fld] = f"${{case.{fld}}}"
|
|
for ni, node in enumerate(template.get("nodes", [])):
|
|
_apply_attr_placeholders(node, ni, ambiguous, attr_varies)
|
|
|
|
sweep_cases = []
|
|
for c in cases:
|
|
values = {}
|
|
for fld in top_varies:
|
|
values[fld] = c.graph.get(fld)
|
|
tv = []
|
|
ctensors = tensors_by_uid(c.graph)
|
|
for uid in sorted(rep_tensors):
|
|
entry = {"uid": uid}
|
|
src = ctensors.get(uid, {})
|
|
for fld in TENSOR_ALWAYS:
|
|
if fld in src:
|
|
entry[fld] = src[fld]
|
|
for fld in tensor_value_varies.get(uid, set()):
|
|
if fld in src:
|
|
entry[fld] = src[fld]
|
|
tv.append(entry)
|
|
values["tensors"] = tv
|
|
attrs = {}
|
|
for ni, node in enumerate(c.graph.get("nodes", [])):
|
|
for attr_key, v in node_attr_items(node, ni, ambiguous):
|
|
if attr_key in attr_varies:
|
|
attrs[attr_key] = v
|
|
if attrs:
|
|
values["attributes"] = attrs
|
|
|
|
meta = dict(c.meta)
|
|
meta.setdefault("format_version", 1)
|
|
meta["reference_source"] = f"c++ integration suite: {c.suite}.{c.case_name}"
|
|
|
|
sweep_cases.append(
|
|
{"id": None, "values": values, "metadata": meta, "_origin": c}
|
|
)
|
|
return template, sweep_cases, None
|
|
|
|
|
|
def _apply_attr_placeholders(
|
|
node: dict, node_index: int, ambiguous: set, attr_varies: set
|
|
):
|
|
def nskey(base):
|
|
return f"n{node_index}__{base}" if base in ambiguous else base
|
|
|
|
for k in list(node.keys()):
|
|
if k in NODE_STRUCTURAL or k in ("inputs", "outputs", "parameters"):
|
|
continue
|
|
if nskey(k) in attr_varies:
|
|
node[k] = f"${{case.attributes.{nskey(k)}}}"
|
|
params = node.get("parameters")
|
|
if isinstance(params, dict):
|
|
for k in list(params.keys()):
|
|
if nskey(f"parameters__{k}") in attr_varies:
|
|
params[k] = f"${{case.attributes.{nskey(f'parameters__{k}')}}}"
|
|
for section in ("inputs", "outputs"):
|
|
sec = node.get(section, {})
|
|
if isinstance(sec, dict):
|
|
for k in list(sec.keys()):
|
|
if not k.endswith("_tensor_uid") and nskey(k) in attr_varies:
|
|
sec[k] = f"${{case.attributes.{nskey(k)}}}"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 3. Verify (round-trip: expand template, compare to original)
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def verify_case(template: dict, entry: dict) -> bool:
|
|
expanded = expand(template, entry["values"])
|
|
original = entry["_origin"].graph
|
|
return canon(expanded) == canon(original)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 4. Writers
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def write_sweep(
|
|
target: Path, bucket: Bucket, template: dict, sweep_cases: list, dry_run: bool
|
|
):
|
|
out_dir = target / bucket.tier / bucket.operation / bucket.topology_name
|
|
cases_out = []
|
|
for e in sweep_cases:
|
|
cases_out.append(
|
|
{"id": e["id"], "values": e["values"], "metadata": e["metadata"]}
|
|
)
|
|
sweep = {"version": 1, "cases": cases_out}
|
|
if not dry_run:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
with open(out_dir / "graph.template.json", "w") as f:
|
|
json.dump(template, f, indent=2)
|
|
f.write("\n")
|
|
with open(out_dir / "sweep.json", "w") as f:
|
|
json.dump(sweep, f, indent=2)
|
|
f.write("\n")
|
|
return out_dir
|
|
|
|
|
|
def write_standalone(target: Path, case: CapturedCase, reason: str, dry_run: bool):
|
|
graph = case.original_graph if case.original_graph is not None else case.graph
|
|
op = derive_operation(graph)
|
|
tensors = tensors_by_uid(graph)
|
|
first = tensors[min(tensors)] if tensors else {}
|
|
dt = graph.get("io_data_type") or first.get("data_type") or "unknown"
|
|
dtok = DTYPE_TOKEN.get(str(dt).lower(), str(dt).lower())
|
|
layout = infer_layout(first.get("dims"), first.get("strides")) or "any"
|
|
bundle_name = sanitize(case.case_name)
|
|
out_dir = target / case.tier / op / layout / dtok / bundle_name
|
|
if not dry_run:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
with open(out_dir / f"{bundle_name}.json", "w") as f:
|
|
json.dump(graph, f, indent=2)
|
|
f.write("\n")
|
|
meta = dict(case.meta)
|
|
meta.setdefault("format_version", 1)
|
|
meta["reference_source"] = (
|
|
f"c++ integration suite: {case.suite}.{case.case_name}"
|
|
)
|
|
meta["standalone_reason"] = reason
|
|
with open(out_dir / f"{bundle_name}.meta.json", "w") as f:
|
|
json.dump(meta, f, indent=2)
|
|
f.write("\n")
|
|
return out_dir
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 5. Main
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def assign_topology_names(buckets: list):
|
|
by_op = defaultdict(list)
|
|
for b in buckets:
|
|
by_op[(b.tier, b.operation)].append(b)
|
|
for _, group in by_op.items():
|
|
group.sort(key=lambda b: b.skeleton_hash)
|
|
for i, b in enumerate(group):
|
|
b.topology_name = "Default" if i == 0 else f"Variant{i + 1}"
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(
|
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
ap.add_argument(
|
|
"--capture-dir",
|
|
type=Path,
|
|
required=True,
|
|
help="root of --capture-bundles output",
|
|
)
|
|
ap.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
required=True,
|
|
help="root of output tree (e.g. integration_test_bundles/)",
|
|
)
|
|
ap.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="report what would be written without writing",
|
|
)
|
|
ap.add_argument(
|
|
"--no-verify",
|
|
action="store_true",
|
|
help="skip round-trip verification (NOT recommended)",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
stats = Stats()
|
|
|
|
cases = discover_captures(args.capture_dir)
|
|
stats.cases_found = len(cases)
|
|
if not cases:
|
|
print("place_bundles: no captured cases found", file=sys.stderr)
|
|
return 1
|
|
|
|
# Canonicalize UIDs by tensor name so that the same logical tensor carries
|
|
# the same UID in every case of a topology. The C++ builder auto-assigns
|
|
# UIDs non-deterministically (GraphTensorIds.hpp iterates an unordered_set),
|
|
# so without this, sibling cases disagree on which UID is Y / MEAN / momentum
|
|
# and can never share a template. Graph and metadata inputs are remapped in
|
|
# lockstep so each tensor keeps its fill spec (canonical_uid_map_by_name).
|
|
for c in cases:
|
|
uid_map = canonical_uid_map_by_name(c.graph)
|
|
c.graph = remap_graph(c.graph, uid_map)
|
|
c.meta = remap_meta_inputs(c.meta, uid_map)
|
|
|
|
grouped: dict = defaultdict(list)
|
|
for c in cases:
|
|
h = skeleton_hash(c.graph)
|
|
c.original_graph = c.graph
|
|
grouped[(c.tier, h)].append(c)
|
|
|
|
buckets = []
|
|
for (tier, h), group in grouped.items():
|
|
buckets.append(
|
|
Bucket(
|
|
skeleton_hash=h,
|
|
tier=tier,
|
|
operation=derive_operation(group[0].graph),
|
|
cases=group,
|
|
)
|
|
)
|
|
stats.buckets = len(buckets)
|
|
assign_topology_names(buckets)
|
|
|
|
topology_map = []
|
|
|
|
for bucket in sorted(buckets, key=lambda b: (b.tier, b.operation, b.topology_name)):
|
|
if len(bucket.cases) == 1:
|
|
reason = "single-case topology (no sweep benefit)"
|
|
out = write_standalone(
|
|
args.output_dir, bucket.cases[0], reason, args.dry_run
|
|
)
|
|
stats.standalone_written += 1
|
|
print(f" standalone: {out} ({reason})", file=sys.stderr)
|
|
continue
|
|
|
|
template, sweep_cases, err = detect_and_build(bucket)
|
|
if err is not None:
|
|
for c in bucket.cases:
|
|
write_standalone(args.output_dir, c, err, args.dry_run)
|
|
stats.standalone_written += 1
|
|
print(
|
|
f" SKIP->standalone {bucket.operation}/{bucket.topology_name} "
|
|
f"({len(bucket.cases)} cases): {err}",
|
|
file=sys.stderr,
|
|
)
|
|
continue
|
|
|
|
assign_case_ids(sweep_cases)
|
|
|
|
if not args.no_verify:
|
|
failed = [e for e in sweep_cases if not verify_case(template, e)]
|
|
if failed:
|
|
for c in bucket.cases:
|
|
write_standalone(
|
|
args.output_dir, c, "round-trip verify failed", args.dry_run
|
|
)
|
|
stats.standalone_written += 1
|
|
stats.verify_fail += len(failed)
|
|
print(
|
|
f" VERIFY FAIL {bucket.operation}/{bucket.topology_name}: "
|
|
f"{len(failed)}/{len(sweep_cases)} cases -> standalone",
|
|
file=sys.stderr,
|
|
)
|
|
continue
|
|
stats.verify_pass += len(sweep_cases)
|
|
|
|
out = write_sweep(args.output_dir, bucket, template, sweep_cases, args.dry_run)
|
|
stats.sweeps_written += 1
|
|
stats.sweep_cases += len(sweep_cases)
|
|
topology_map.append(
|
|
{
|
|
"skeleton_hash": bucket.skeleton_hash,
|
|
"tier": bucket.tier,
|
|
"operation": bucket.operation,
|
|
"topology_name": bucket.topology_name,
|
|
"path": str(out),
|
|
"case_count": len(sweep_cases),
|
|
}
|
|
)
|
|
print(
|
|
f" sweep: {bucket.tier}/{bucket.operation}/{bucket.topology_name} "
|
|
f"({len(sweep_cases)} graphs -> 1 sweep)",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
if not args.dry_run and topology_map:
|
|
report_dir = args.output_dir.parent / ".migration_reports"
|
|
report_dir.mkdir(parents=True, exist_ok=True)
|
|
map_path = report_dir / "topology_map.json"
|
|
with open(map_path, "w") as f:
|
|
json.dump({"version": 1, "topologies": topology_map}, f, indent=2)
|
|
f.write("\n")
|
|
print(f" topology map: {map_path}", file=sys.stderr)
|
|
|
|
print("== place_bundles ==", file=sys.stderr)
|
|
print(f" capture dir: {args.capture_dir}", file=sys.stderr)
|
|
print(f" cases found: {stats.cases_found}", file=sys.stderr)
|
|
print(f" skeletons: {stats.buckets}", file=sys.stderr)
|
|
print(
|
|
f" sweeps written: {stats.sweeps_written} ({stats.sweep_cases} cases)",
|
|
file=sys.stderr,
|
|
)
|
|
print(f" standalone: {stats.standalone_written}", file=sys.stderr)
|
|
if not args.no_verify:
|
|
print(f" verify pass: {stats.verify_pass}", file=sys.stderr)
|
|
print(f" verify fail: {stats.verify_fail}", file=sys.stderr)
|
|
total_out = stats.sweep_cases + stats.standalone_written
|
|
print(f" accounted graphs: {total_out} / {stats.cases_found}", file=sys.stderr)
|
|
|
|
if total_out != stats.cases_found:
|
|
print(
|
|
f" ERROR: {stats.cases_found - total_out} graphs unaccounted for!",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|