Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/Tensile/backends/ductile_backend.py
T
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

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