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
381 lines
16 KiB
Python
381 lines
16 KiB
Python
# Copyright Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
DuctileBackend: Genetic algorithm-based parameter search strategy.
|
|
|
|
This backend uses a genetic algorithm to search the fork parameter space,
|
|
iteratively evaluating promising candidates and culling poor performers.
|
|
It requires the Ductile GA modules.
|
|
"""
|
|
|
|
from typing import List, Dict, Any, Callable, Tuple
|
|
import numpy as np
|
|
import csv
|
|
import functools
|
|
import os
|
|
import shutil
|
|
from copy import deepcopy
|
|
|
|
from Tensile.Common import Path, print1, printExit, printWarning
|
|
from Tensile.Common.GlobalParameters import globalParameters
|
|
from Tensile.SolutionStructs.Naming import getKernelFileBase, getSolutionNameMin
|
|
from Tensile.KernelWriterAssembly import KernelWriterAssembly
|
|
from .base import OptimizationBackend
|
|
|
|
|
|
from Tensile.ductile import config as ductile_config
|
|
from Tensile.ductile.core import SearchSpace, Selection, Crossover, Mutation, Mating, Survival
|
|
from Tensile.ductile.algorithm import GeneticAlgorithm
|
|
|
|
|
|
|
|
def _generate_single_solution_with_groups(perm, problemType, constantParams, assembler, debugConfig, isaInfoMap, silent=False):
|
|
"""Generate a single solution from a permutation, handling group_ parameter expansion."""
|
|
from Tensile.BenchmarkProblems import _build_and_validate_solution
|
|
|
|
solution = {
|
|
"ProblemType": deepcopy(problemType.state),
|
|
"ISA": next(iter(isaInfoMap.keys()))
|
|
}
|
|
solution.update(constantParams)
|
|
solution.update(perm)
|
|
|
|
# Expand group_ parameters
|
|
for p in list(perm.keys()):
|
|
if p.startswith("group_"):
|
|
solution.update(perm[p])
|
|
if p in solution:
|
|
del solution[p]
|
|
|
|
return _build_and_validate_solution(solution, assembler, debugConfig, isaInfoMap, silent=silent)
|
|
|
|
|
|
def _validate_solution(problemType, constantParams, assembler, debugConfig, isaInfoMap, perm, get_kernel_src=False):
|
|
"""Validate a solution candidate for GA SearchSpace constraint checking."""
|
|
solution_object = _generate_single_solution_with_groups(
|
|
perm, problemType, constantParams, assembler, debugConfig, isaInfoMap, silent=True
|
|
)
|
|
if solution_object is None:
|
|
return False
|
|
|
|
kernelWriterAssembly = KernelWriterAssembly(assembler, debugConfig)
|
|
try:
|
|
kernelWriterAssembly._initKernel(solution_object, {}, {})
|
|
if get_kernel_src:
|
|
kernelWriterAssembly._getKernelSource(solution_object)
|
|
except RuntimeError:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _generate_ga_solutions(problemType, constantParams, individuals, assembler, debugConfig, isaInfoMap):
|
|
"""Match Ductile/BenchmarkProblems.py generateGASolutions behavior.
|
|
|
|
Important: returns a list aligned with individuals, inserting None for duplicates
|
|
or invalid candidates so the GA index mapping remains stable.
|
|
"""
|
|
solutions = []
|
|
solutionSet = set()
|
|
baseSet = set()
|
|
|
|
for perm in individuals:
|
|
solutionObject = _generate_single_solution_with_groups(
|
|
perm, problemType, constantParams, assembler, debugConfig, isaInfoMap
|
|
)
|
|
|
|
if solutionObject is not None:
|
|
base = getKernelFileBase(debugConfig.splitGSU, solutionObject)
|
|
if solutionObject not in solutionSet and base not in baseSet:
|
|
solutionSet.add(solutionObject)
|
|
baseSet.add(base)
|
|
solutions.append(solutionObject)
|
|
else:
|
|
solutions.append(None)
|
|
else:
|
|
solutions.append(None)
|
|
|
|
return solutions
|
|
|
|
|
|
def _read_and_validate_scores_csv(results_filename: str, expected_cols: List[str]) -> np.ndarray:
|
|
"""Read benchmark scores from CSV and validate expected columns and row data.
|
|
|
|
Validates:
|
|
- non-empty CSV with a header row
|
|
- presence of all expected columns
|
|
- each data row has all expected columns
|
|
- each selected cell is parseable as float
|
|
"""
|
|
with open(results_filename, "r") as f:
|
|
reader = csv.reader(f)
|
|
try:
|
|
headers = [h.strip() for h in next(reader)]
|
|
except StopIteration:
|
|
printExit(f"BenchmarkProblems: Empty CSV results file: {results_filename}")
|
|
|
|
if not headers:
|
|
printExit(f"BenchmarkProblems: Missing CSV header row in results file: {results_filename}")
|
|
|
|
col_indices = []
|
|
for solution_name in expected_cols:
|
|
if solution_name not in headers:
|
|
printExit(
|
|
"BenchmarkProblems: Missing expected result column for valid solution "
|
|
f"'{solution_name}' in {results_filename}"
|
|
)
|
|
col_indices.append(headers.index(solution_name))
|
|
|
|
rows = []
|
|
for row_num, row in enumerate(reader, start=2):
|
|
row_scores = []
|
|
for solution_name, col_idx in zip(expected_cols, col_indices):
|
|
if col_idx >= len(row):
|
|
printExit(
|
|
"BenchmarkProblems: Malformed CSV row in results file "
|
|
f"'{results_filename}': row {row_num} is missing value for column "
|
|
f"'{solution_name}'"
|
|
)
|
|
try:
|
|
row_scores.append(float(row[col_idx]))
|
|
except ValueError:
|
|
printExit(
|
|
"BenchmarkProblems: Non-float result value in results file "
|
|
f"'{results_filename}': row {row_num}, column '{solution_name}', "
|
|
f"value '{row[col_idx]}'"
|
|
)
|
|
rows.append(row_scores)
|
|
|
|
if not rows:
|
|
printExit(f"BenchmarkProblems: No data rows found in results file: {results_filename}")
|
|
|
|
return np.array(rows, dtype=np.float32)
|
|
|
|
|
|
class DuctileBackend(OptimizationBackend):
|
|
"""Backend for genetic algorithm-based parameter optimization.
|
|
|
|
Uses Ductile's GA implementation to iteratively search the fork parameter space,
|
|
focusing effort on promising regions and avoiding exhaustive enumeration.
|
|
"""
|
|
|
|
def run(self,
|
|
backend_config: Dict[str, Any],
|
|
benchmark_config: Dict[str, Any],
|
|
benchmark_runner: Callable[[List[Any]], Tuple[str, int]],
|
|
cacheValid: bool = False,
|
|
buildOnly: bool = False) -> None:
|
|
"""Execute GA optimization loop.
|
|
|
|
Creates GA instance with _evaluate callback, runs optimization,
|
|
and performs post-optimization verification.
|
|
|
|
Args:
|
|
backend_config: Configuration dict containing backend-specific settings.
|
|
benchmark_config: Backend step configuration containing:
|
|
- forkParams, constantParams, paramGroups
|
|
- problemType, assembler, debugConfig, isaInfoMap
|
|
- sourcePath
|
|
benchmark_runner: Function returning (resultsFileName, returncode)
|
|
cacheValid: DuctileBackend does not support caching; warns and forces False
|
|
buildOnly: DuctileBackend does not support build-only mode; warns and forces False
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
source_path = benchmark_config.get("sourcePath", None)
|
|
|
|
if cacheValid:
|
|
printWarning("DuctileBackend: cacheValid is not supported; running with cacheValid=False")
|
|
if buildOnly:
|
|
printWarning("DuctileBackend: buildOnly is not supported; running full benchmark")
|
|
|
|
if "benchmarkStep" not in benchmark_config or benchmark_config["benchmarkStep"] is None:
|
|
raise ValueError("BenchmarkProblems: Missing required backend config key: benchmarkStep")
|
|
|
|
benchmark_step = benchmark_config["benchmarkStep"]
|
|
fork_params = benchmark_step.forkParams.copy()
|
|
param_groups = benchmark_step.paramGroups
|
|
constant_params = benchmark_step.constantParams
|
|
problem_type = benchmark_config.get("problemType")
|
|
assembler = benchmark_config.get("assembler")
|
|
debug_config = benchmark_config.get("debugConfig")
|
|
isa_info_map = benchmark_config.get("isaInfoMap")
|
|
|
|
if problem_type is None or assembler is None or debug_config is None or isa_info_map is None:
|
|
raise ValueError("DuctileBackend: Missing required config keys: problemType, assembler, debugConfig, isaInfoMap")
|
|
|
|
validate_fn = functools.partial(_validate_solution, problem_type, constant_params, assembler, debug_config, isa_info_map)
|
|
|
|
# Merge once per run from defaults.yaml + overrides.
|
|
merged_config = ductile_config.update(backend_config)
|
|
|
|
# Apply group_x logic from Ductile/BenchmarkProblems.py.
|
|
for i, group in enumerate(param_groups):
|
|
if len(group) == 1:
|
|
for p in group:
|
|
constant_params.update(p)
|
|
else:
|
|
fork_params[f"group_{i}"] = group
|
|
|
|
space = SearchSpace(
|
|
fork_params,
|
|
valid=validate_fn,
|
|
max_iters=merged_config["max_iters"]
|
|
)
|
|
|
|
selection = Selection.get(**ductile_config.populate(merged_config, "selection"))
|
|
crossover = Crossover.get(**ductile_config.populate(merged_config, "crossover"))
|
|
mutation = Mutation(space, **merged_config["mutation"])
|
|
mating = Mating(
|
|
space=space,
|
|
selection=selection,
|
|
crossover=crossover,
|
|
mutation=mutation,
|
|
max_iters=merged_config["max_iters"]
|
|
)
|
|
survival = Survival.get(**ductile_config.populate(merged_config, "survival"))
|
|
|
|
print1(f"# DuctileBackend: Starting GA optimization")
|
|
|
|
def _evaluate(individuals):
|
|
"""Fitness callback for GA - returns multi-dimensional fitness array.
|
|
|
|
Takes GA individuals (parameter dicts), benchmarks them, reads CSV results,
|
|
and returns fitness scores array matching multi-dimensional benchmark results.
|
|
Uses Ductile/BenchmarkProblems.py error handling patterns.
|
|
|
|
Args:
|
|
individuals: List of parameter dicts from GA population
|
|
|
|
Returns:
|
|
2D numpy array of fitness scores with shape (n_sizes, len(individuals))
|
|
where n_sizes is the number of benchmark problem sizes
|
|
"""
|
|
# Ductile path uses Ductile-native generation to preserve per-individual alignment and duplicate handling.
|
|
converted = _generate_ga_solutions(
|
|
problem_type,
|
|
constant_params,
|
|
individuals,
|
|
assembler,
|
|
debug_config,
|
|
isa_info_map,
|
|
)
|
|
solutions = []
|
|
for idx, solution in enumerate(converted):
|
|
if solution is not None:
|
|
solution.solIdx = idx
|
|
solutions.append(solution)
|
|
|
|
print1(f"# DuctileBackend: Generated {len(solutions)} valid solutions from {len(individuals)} candidates")
|
|
|
|
# Ductile always clears previous run artifacts before re-benchmarking each population.
|
|
if source_path and os.path.isdir(source_path):
|
|
shutil.rmtree(source_path)
|
|
|
|
# Benchmark solutions - Ductile forces no cache and no build-only.
|
|
results_filename, returncode = benchmark_runner(solutions, isCached=False, buildOnly=False)
|
|
|
|
if not results_filename or not os.path.isfile(results_filename):
|
|
printExit(f"BenchmarkProblems: Expected results file does not exist: {results_filename}")
|
|
|
|
expected_cols = [getSolutionNameMin(solution, debug_config.splitGSU) for solution in solutions]
|
|
if len(set(expected_cols)) != len(expected_cols):
|
|
printExit("BenchmarkProblems: Duplicate solution names detected in expected results columns")
|
|
|
|
scores = _read_and_validate_scores_csv(results_filename, expected_cols)
|
|
|
|
# Extract scores from CSV
|
|
n_sizes = scores.shape[0]
|
|
if n_sizes == 1:
|
|
scores = scores[None, ...]
|
|
|
|
# Build multi-dimensional fitness array: (n_sizes, len(individuals))
|
|
# nGFlops[i, j] = performance of solution j at problem size i
|
|
nGFlops = np.zeros((n_sizes, len(individuals)), dtype=np.float32)
|
|
idxs = [si.solIdx for si in solutions]
|
|
nGFlops[:, idxs] = scores
|
|
|
|
return nGFlops
|
|
|
|
|
|
root = Path(benchmark_config.get("rootPath", "."))
|
|
cfg_name = benchmark_config["configName"]
|
|
if benchmark_config.get("totalBenchmarkSteps", 1) == 1:
|
|
log_file = root / f"{cfg_name}-optimization.log"
|
|
else:
|
|
log_file = root / f"cfg-{cfg_name}__step-{benchmark_config.get('benchmarkStepIdx', 0):02d}__optimization.log"
|
|
|
|
ckpt_path = root / f"step-{benchmark_config.get('benchmarkStepIdx', 0):02d}__ductile.checkpoint"
|
|
|
|
# Create GA instance with evaluate callback
|
|
ga = GeneticAlgorithm(
|
|
space,
|
|
mating,
|
|
evaluate=_evaluate,
|
|
survival=survival,
|
|
pop_size=merged_config["pop_size"],
|
|
n_gen=merged_config["n_gen"],
|
|
soo=merged_config["soo"],
|
|
period=merged_config["period"],
|
|
tol=merged_config["tol"],
|
|
div_thr=merged_config["div_thr"],
|
|
seed=merged_config["seed"],
|
|
verbose=merged_config["verbose"],
|
|
log_file=log_file,
|
|
checkpoint_path=ckpt_path,
|
|
weights=merged_config["weights"],
|
|
weight_beta=merged_config["weight_beta"],
|
|
)
|
|
|
|
if ckpt_path.is_file():
|
|
printWarning(f"DuctileBackend: Found existing checkpoint at {ckpt_path}, resuming optimization from checkpoint")
|
|
try:
|
|
ga.load(ckpt_path)
|
|
except Exception as e:
|
|
printWarning(f"DuctileBackend: Failed to load checkpoint: {e}. Starting fresh optimization.")
|
|
|
|
# Run GA optimization - GA internally calls _evaluate and orchestrates the loop
|
|
best, _ = ga.optimize()
|
|
|
|
netv_original = globalParameters.get("NumElementsToValidate", 128)
|
|
try:
|
|
# Temporarily override validation
|
|
globalParameters["NumElementsToValidate"] = merged_config.get("n_elements_to_validate", 0)
|
|
|
|
# Re-evaluate best solutions
|
|
res = ga.evaluate(best)
|
|
|
|
# Check for validation failures (-1 indicates failed validation)
|
|
if (res == -1).any():
|
|
if res.ndim == 1:
|
|
res = res[None, ...]
|
|
|
|
# Find solutions that passed validation
|
|
keep = (res > 0).all(axis=0)
|
|
|
|
if keep.sum() == 0:
|
|
printExit(f"DuctileBackend:No solutions passed the verification stage")
|
|
elif keep.sum() < len(best):
|
|
printWarning(f"# DuctileBackend: {keep.sum()}/{len(best)} passed verification")
|
|
best = [best[i] for i in np.where(keep)[0]]
|
|
|
|
# Re-evaluate filtered set
|
|
res = ga.evaluate(best)
|
|
|
|
finally:
|
|
# Always restore original validation setting
|
|
globalParameters["NumElementsToValidate"] = netv_original
|
|
|
|
def supports_solution_pool(self) -> bool:
|
|
"""DuctileBackend does not support loading solutions from pool files.
|
|
|
|
The GA-driven backend requires generating solutions through its own
|
|
optimization loop rather than using pre-computed solution pools.
|
|
|
|
Returns:
|
|
False - DuctileBackend does not use solution pool files
|
|
"""
|
|
return False
|
|
|