[rocprofiler-compute] improve config management system (#2359)
This commit is contained in:
@@ -23,7 +23,7 @@ repos:
|
||||
hooks:
|
||||
- id: hash-check
|
||||
name: Hash consistency check
|
||||
entry: bash -lc 'cd projects/rocprofiler-compute && python3 tools/config_management/hash_checker.py'
|
||||
entry: bash -lc 'cd projects/rocprofiler-compute && python3 src/utils/hash_checker.py'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from utils/unified_sets.yaml. Generated by utils/split_config.py
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from utils/unified_sets.yaml. Generated by utils/split_config.py
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from utils/unified_sets.yaml. Generated by utils/split_config.py
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from utils/unified_sets.yaml. Generated by utils/split_config.py
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from utils/unified_sets.yaml. Generated by utils/split_config.py
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from utils/unified_sets.yaml. Generated by utils/split_config.py
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
|
||||
+1
-1
@@ -139,4 +139,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-19
@@ -43,27 +43,16 @@ from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from . import hash_manager # type: ignore
|
||||
except Exception:
|
||||
import importlib.util
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2] # rocprofiler-compute/
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
_HERE = Path(__file__).resolve().parent
|
||||
_SPEC = importlib.util.spec_from_file_location(
|
||||
"hash_manager", str(_HERE / "hash_manager.py")
|
||||
)
|
||||
hash_manager = importlib.util.module_from_spec(_SPEC) # type: ignore[assignment]
|
||||
assert _SPEC and _SPEC.loader is not None
|
||||
_SPEC.loader.exec_module(hash_manager) # type: ignore[attr-defined]
|
||||
# ---------------------------------------------------------------------------
|
||||
from tools.config_management import hash_manager # noqa: E402
|
||||
|
||||
# Subproject root: .../projects/rocprofiler-compute
|
||||
SUBROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
CONFIGS_ROOT: Path = SUBROOT / "src" / "rocprof_compute_soc" / "analysis_configs"
|
||||
HASH_FILE: Path = SUBROOT / "tools" / "config_management" / ".config_hashes.json"
|
||||
CONFIGS_ROOT: Path = PROJECT_ROOT / "src" / "rocprof_compute_soc" / "analysis_configs"
|
||||
HASH_FILE: Path = PROJECT_ROOT / "src" / "utils" / ".config_hashes.json"
|
||||
TEMPLATE_FILE: Path = (
|
||||
SUBROOT / "tools" / "config_management" / "analysis_config_template.yaml"
|
||||
PROJECT_ROOT / "tools" / "config_management" / "gfx9_config_template.yaml"
|
||||
)
|
||||
|
||||
|
||||
@@ -73,7 +62,7 @@ TEMPLATE_FILE: Path = (
|
||||
def _latest_arch(template_file: Path) -> str:
|
||||
if not template_file.is_file():
|
||||
return ""
|
||||
with open(template_file, "r", encoding="utf-8") as f:
|
||||
with open(template_file, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return str(data.get("latest_arch") or "")
|
||||
|
||||
@@ -24,25 +24,91 @@
|
||||
##############################################################################
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
HASH_DB = PROJECT_ROOT / "src/utils/.config_hashes.json"
|
||||
ANALYSIS_CONFIGS = PROJECT_ROOT / "src/rocprof_compute_soc/analysis_configs"
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason=(
|
||||
"TODO: Skip this test until we use "
|
||||
"tools/config_management/.config.hashes.json for testing"
|
||||
def md5(path: Path) -> str:
|
||||
return hashlib.md5(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_config_hashes_match_files() -> None:
|
||||
assert HASH_DB.exists(), f"Missing hash DB: {HASH_DB}"
|
||||
assert ANALYSIS_CONFIGS.exists(), (
|
||||
f"Missing analysis configs dir: {ANALYSIS_CONFIGS}"
|
||||
)
|
||||
)
|
||||
def test_modification_time():
|
||||
# Ensure hash map consistency
|
||||
hash_path = Path("tools/autogen_hash.yaml")
|
||||
with open(hash_path) as f:
|
||||
hash_map = yaml.safe_load(f)
|
||||
for file, hash in hash_map.items():
|
||||
file_hash = hashlib.sha256(Path(file).read_bytes()).hexdigest()
|
||||
assert file_hash == hash, (
|
||||
f"Hash mismatch for {file}: expected {hash}, got {file_hash}"
|
||||
)
|
||||
|
||||
with HASH_DB.open() as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "archs" in data, "Hash DB missing 'archs' key"
|
||||
assert isinstance(data["archs"], dict)
|
||||
|
||||
failures = []
|
||||
|
||||
for arch, arch_data in data["archs"].items():
|
||||
arch_dir = ANALYSIS_CONFIGS / arch
|
||||
if not arch_dir.exists():
|
||||
failures.append(f"Arch directory missing: {arch_dir}")
|
||||
continue
|
||||
|
||||
# -------------------------
|
||||
# Panel YAMLs
|
||||
# -------------------------
|
||||
files = arch_data.get("files", {})
|
||||
if not isinstance(files, dict):
|
||||
failures.append(f"'files' for {arch} is not a dict")
|
||||
continue
|
||||
|
||||
for rel_path, expected_hash in files.items():
|
||||
panel_path = arch_dir / rel_path
|
||||
if not panel_path.exists():
|
||||
failures.append(f"Missing panel file: {panel_path}")
|
||||
continue
|
||||
|
||||
actual_hash = md5(panel_path)
|
||||
if actual_hash != expected_hash:
|
||||
failures.append(
|
||||
f"[{arch}] Panel hash mismatch: {panel_path}\n"
|
||||
f" expected: {expected_hash}\n"
|
||||
f" actual: {actual_hash}"
|
||||
)
|
||||
|
||||
# -------------------------
|
||||
# Delta YAML (if any)
|
||||
# -------------------------
|
||||
delta_hash = arch_data.get("delta_hash")
|
||||
|
||||
if delta_hash is not None:
|
||||
delta_dir = arch_dir / "config_delta"
|
||||
if not delta_dir.exists():
|
||||
failures.append(f"[{arch}] Missing config_delta directory")
|
||||
continue
|
||||
|
||||
# Exactly one *_diff.yaml should exist
|
||||
delta_files = list(delta_dir.glob("*_diff.yaml"))
|
||||
if len(delta_files) != 1:
|
||||
failures.append(
|
||||
f"[{arch}] Expected exactly one delta file, found "
|
||||
f"{len(delta_files)} in {delta_dir}"
|
||||
)
|
||||
continue
|
||||
|
||||
delta_path = delta_files[0]
|
||||
actual_delta_hash = md5(delta_path)
|
||||
|
||||
if actual_delta_hash != delta_hash:
|
||||
failures.append(
|
||||
f"[{arch}] Delta hash mismatch: {delta_path}\n"
|
||||
f" expected: {delta_hash}\n"
|
||||
f" actual: {actual_delta_hash}"
|
||||
)
|
||||
|
||||
if failures:
|
||||
pytest.fail("Hash consistency failures:\n\n" + "\n".join(failures))
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from tools/unified_config.yaml. Generated by tools/split_config.py
|
||||
{}
|
||||
@@ -1,500 +1,276 @@
|
||||
# Architecture Configuration Workflow
|
||||
# ROCProfiler-Compute Configuration Management
|
||||
|
||||
This document explains the master workflow system for managing architecture-specific metric configurations.
|
||||
This directory contains the authoritative configuration-management system for ROCProfiler-Compute analysis configurations.
|
||||
|
||||
## Overview
|
||||
It is designed to guarantee:
|
||||
|
||||
The workflow system manages changes to architecture configurations located in `src/rocprof_compute_soc/analysis_configs/gfx<arch>/`. It handles:
|
||||
- **Structural correctness** across GPU architectures
|
||||
- **Deterministic deltas** relative to a single latest architecture
|
||||
- **Byte-level immutability** enforced via hashes
|
||||
- **Safe promotion** of a new latest architecture with rollback
|
||||
- **CI enforcement** of all invariants
|
||||
|
||||
- **Metric changes** (additions, deletions, modifications)
|
||||
- **Metric description changes** (plain text + RST documentation)
|
||||
- **New architecture additions**
|
||||
- **Template updates**
|
||||
- **Config delta generation** for version control
|
||||
|
||||
## Files Overview
|
||||
|
||||
### Core Scripts
|
||||
|
||||
1. **`master_config_workflow_script.py`** - Main orchestrator script
|
||||
2. **`hash_manager.py`** - Tracks file changes via MD5 hashes
|
||||
3. **`metric_description_manager.py`** - Syncs metric descriptions across files
|
||||
4. **`config_workflow.yaml`** - Configuration file
|
||||
5. **`parse_config_template.py`** - Parses base config template from latest arch
|
||||
6. **`generate_config_deltas.py`** - Generates config deltas between two archs
|
||||
7. **`apply_config_deltas.py`** - Applies config deltas to genearte new arch configs
|
||||
8. **`verify_against_config_template.py`** - Validates configs against template
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Initial Setup (not needed following first commit)
|
||||
|
||||
1. Create the hash database:
|
||||
```bash
|
||||
python hash_manager.py --compute-all src/rocprof_compute_soc/analysis_configs
|
||||
```
|
||||
|
||||
2. Ensure `analysis_config_template.yaml` has metadata:
|
||||
```yaml
|
||||
latest_arch: gfx950
|
||||
panels:
|
||||
- file: top_stats.yaml
|
||||
panel_id: 0
|
||||
...
|
||||
```
|
||||
|
||||
### Making Changes
|
||||
|
||||
Simply run the master workflow after making any changes:
|
||||
All workflows are orchestrated by a single sequential driver script:
|
||||
|
||||
```bash
|
||||
python master_config_workflow_script.py
|
||||
tools/config_management/master_config_workflow_script.py
|
||||
```
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```bash
|
||||
rocprofiler-compute/
|
||||
├── src/rocprof_compute_soc/
|
||||
│ └── analysis_configs/
|
||||
│ ├── gfx908/
|
||||
│ │ ├── 0000_top_stats.yaml
|
||||
│ │ └── config_delta/
|
||||
│ │ └── <latest_arch>_diff.yaml
|
||||
│ ├── gfx90a/
|
||||
│ ├── gfx940/
|
||||
│ ├── gfx950/ # latest_arch
|
||||
│ └── gfx9_config_template.yaml # single source of truth
|
||||
│
|
||||
├── src/util/
|
||||
│ ├── hash_checker.py
|
||||
│ ├── .config_hashes.json
|
||||
│
|
||||
└── tools/config_management/
|
||||
├── master_config_workflow_script.py
|
||||
├── parse_config_template.py
|
||||
├── verify_against_config_template.py
|
||||
├── generate_config_deltas.py
|
||||
├── apply_config_deltas.py
|
||||
├── hash_manager.py
|
||||
├── TESTING.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
### Latest Architecture
|
||||
|
||||
- Exactly one architecture is considered *latest*
|
||||
- Defined in:
|
||||
```bash
|
||||
src/rocprof_compute_soc/analysis_configs/gfx9_config_template.yaml
|
||||
```
|
||||
|
||||
### Panel YAMLs
|
||||
|
||||
- Live under:
|
||||
```bash
|
||||
analysis_configs/<arch>/*.yaml
|
||||
```
|
||||
- Must conform strictly to the template schema
|
||||
- Are edited in-place using ruamel.yaml round-trip mode
|
||||
|
||||
### Delta YAMLs
|
||||
|
||||
- Represent differences from latest → older architecture
|
||||
- Live under:
|
||||
```bash
|
||||
analysis_configs/<older_arch>/config_delta/
|
||||
```
|
||||
- Exactly one delta file per arch
|
||||
- Always named:
|
||||
```bash
|
||||
<latest_arch>_diff.yaml
|
||||
```
|
||||
|
||||
### Hash Database
|
||||
|
||||
- Stored at:
|
||||
```bash
|
||||
src/utils/.config_hashes.json
|
||||
```
|
||||
- Records:
|
||||
- md5 hashes of panel YAMLs per arch
|
||||
- md5 hash of the delta YAML (or null for latest)
|
||||
- Machine-generated only
|
||||
- Enforced in CI and pytest
|
||||
|
||||
## Architecture Diagram (End-to-End Flow)
|
||||
```pqsql
|
||||
┌──────────────────────────┐
|
||||
│ analysis_configs/ │
|
||||
│ gfx9_config_template │
|
||||
└───────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────┐
|
||||
│ verify_against_config_template│
|
||||
│ (structural validation) │
|
||||
└───────────┬───────────────────┘
|
||||
│
|
||||
┌───────────────────┴───────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────────┐ ┌──────────────────────┐
|
||||
│ edit-existing mode │ │ promotion mode │
|
||||
│ (local dev only) │ │ (authoritative path) │
|
||||
└─────────┬──────────┘ └──────────┬───────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────────┐ ┌─────────────────────────────┐
|
||||
│ generate / apply │ │ parse_config_template.py │
|
||||
│ deltas manually │ │ (update latest_arch) │
|
||||
└────────────────────┘ └──────────┬──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ generate_config_deltas.py │
|
||||
│ latest → all older arches │
|
||||
│ (<latest>_diff.yaml only) │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ verify_against_config_template │
|
||||
│ (post-promotion validation) │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ hash_manager.py --compute-all │
|
||||
│ (new steady state) │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ hash_checker.py │
|
||||
│ (semantic consistency) │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Contributor Quick Start
|
||||
|
||||
> [!NOTE]
|
||||
> **Required Python Dependency**
|
||||
> This configuration management system requires the `ruamel.yaml` Python package.
|
||||
> It is used to safely modify YAML files while preserving comments, ordering,
|
||||
> and formatting. The workflow scripts will not function correctly without it.
|
||||
>
|
||||
> Install it via:
|
||||
> ```bash
|
||||
> pip install ruamel.yaml
|
||||
> ```
|
||||
|
||||
### 1. Validate the current state
|
||||
|
||||
Before making **any** config changes:
|
||||
```bash
|
||||
python tools/config_management/master_config_workflow_script.py --validate-only
|
||||
```
|
||||
|
||||
This must pass.
|
||||
|
||||
### 2. Editing an existing architecture (most common)
|
||||
|
||||
Edit panel YAMLs **directly** under:
|
||||
```bash
|
||||
src/rocprof_compute_soc/analysis_configs/<arch>/
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Preserve structure
|
||||
- Preserve ordering
|
||||
- Use multiline `>-` formatting for metric descriptions
|
||||
- Do **not** regenerate entire files
|
||||
|
||||
After editing:
|
||||
```bash
|
||||
python tools/config_management/master_config_workflow_script.py --validate-only
|
||||
```
|
||||
|
||||
### 3. Generating or applying deltas (advanced / optional)
|
||||
|
||||
For local experimentation only:
|
||||
```bash
|
||||
python tools/config_management/master_config_workflow_script.py --edit-existing
|
||||
```
|
||||
|
||||
This mode:
|
||||
|
||||
- never updates the template
|
||||
- never updates hashes
|
||||
- always re-validates after application
|
||||
|
||||
### 4. Promoting a new latest architecture (rare, gated)
|
||||
|
||||
Promotion changes **global invariants** and must use the master script:
|
||||
```bash
|
||||
python tools/config_management/master_config_workflow_script.py --promote <latest_arch>
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Detect what changed
|
||||
- Prompt you for confirmation
|
||||
- Apply changes
|
||||
- Validate results
|
||||
- Update all necessary files
|
||||
|
||||
### Dry Run Mode
|
||||
1. Update `latest_arch` in the template
|
||||
2. Regenerate deltas for all older arches
|
||||
3. Remove stale delta files
|
||||
4. Re-validate everything
|
||||
5. Rebuild the hash database
|
||||
6. Verify semantic consistency
|
||||
|
||||
To see what would happen without making changes:
|
||||
If anything fails:
|
||||
|
||||
- all changes are rolled back
|
||||
- no partial state remains
|
||||
|
||||
### 5. Hash checks (fast local / CI)
|
||||
```bash
|
||||
python master_config_workflow_script.py --dry-run
|
||||
python tools/config_management/master_config_workflow_script.py --hash-only
|
||||
```
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
### Scenario A: Add Metrics to Latest Arch (gfx950)
|
||||
|
||||
**Method 1: Direct Edit**
|
||||
|
||||
1. Edit `src/rocprof_compute_soc/analysis_configs/gfx950/0700_wavefront.yaml`
|
||||
2. Add your metric to the appropriate table
|
||||
3. Add description to `metrics_description` section
|
||||
4. Run: `python master_config_workflow_script.py`
|
||||
5. Answer prompts
|
||||
|
||||
**Method 2: Using Delta**
|
||||
|
||||
1. Create `src/rocprof_compute_soc/analysis_configs/gfx950/config_delta/gfx955_diff.yaml`:
|
||||
```yaml
|
||||
Addition:
|
||||
- Panel Config:
|
||||
id: 700
|
||||
title: Wavefront
|
||||
metric_tables:
|
||||
- metric_table:
|
||||
id: 701
|
||||
title: Wavefront Launch Stats
|
||||
metrics:
|
||||
- New Metric:
|
||||
avg: AVG(something)
|
||||
unit: Units
|
||||
metric_descriptions:
|
||||
New Metric:
|
||||
plain: Description text
|
||||
rst: >- # Optional
|
||||
Description with :ref:`RST markup <link>`
|
||||
|
||||
Deletion:
|
||||
[]
|
||||
|
||||
Modification:
|
||||
[]
|
||||
```
|
||||
|
||||
2. Run: `python master_config_workflow_script.py`
|
||||
|
||||
**What Happens:**
|
||||
- Changes applied to gfx950
|
||||
- Template updated
|
||||
- Deltas regenerated for all previous archs (gfx940, gfx941, etc.)
|
||||
- Metric descriptions synced to:
|
||||
- `tools/per_arch_metric_definitions/gfx950_metrics_description.yaml`
|
||||
- `docs/data/metrics_description.yaml`
|
||||
- All archs validated
|
||||
- Hashes updated
|
||||
|
||||
### Scenario B: Modify Metrics in Older Arch (gfx940)
|
||||
|
||||
**Method 1: Direct Edit**
|
||||
|
||||
1. Edit `src/rocprof_compute_soc/analysis_configs/gfx940/0700_wavefront.yaml`
|
||||
2. Make your changes
|
||||
3. Run: `python master_config_workflow_script.py`
|
||||
|
||||
**Method 2: Using Delta**
|
||||
|
||||
1. Create `src/rocprof_compute_soc/analysis_configs/gfx940/config_delta/gfx950_diff.yaml`
|
||||
2. Run: `python master_config_workflow_script.py`
|
||||
|
||||
**What Happens:**
|
||||
- Changes applied to gfx940 only
|
||||
- Validated against template (must still match structure)
|
||||
- Metric descriptions synced to `tools/per_arch_metric_definitions/gfx940_metrics_description.yaml`
|
||||
- Hashes updated for gfx940 only
|
||||
|
||||
### Scenario C: Add New Architecture (gfx955)
|
||||
|
||||
**Method 1: Create Directory with YAMLs**
|
||||
|
||||
1. Create `src/rocprof_compute_soc/analysis_configs/gfx955/`
|
||||
2. Copy/create YAML files
|
||||
3. Run: `python master_config_workflow_script.py`
|
||||
4. Confirm this is the new latest arch
|
||||
|
||||
**Method 2: Using Delta from Latest**
|
||||
|
||||
1. Create delta showing differences from gfx950
|
||||
2. Place in `src/rocprof_compute_soc/analysis_configs/gfx955/config_delta/gfx955_diff.yaml`
|
||||
3. Run: `python master_config_workflow_script.py`
|
||||
4. Confirm this is the new latest arch
|
||||
|
||||
**What Happens:**
|
||||
- gfx955 becomes new latest arch
|
||||
- Template updated with gfx955 as source
|
||||
- Deltas generated: gfx955 → gfx950, gfx955 → gfx940, etc.
|
||||
- All archs validated
|
||||
- Metric descriptions synced
|
||||
- Hashes updated
|
||||
|
||||
### Scenario D: Update Metric Descriptions Only
|
||||
|
||||
1. Edit description in config YAML:
|
||||
```yaml
|
||||
metrics_description:
|
||||
Grid Size: "Updated description text"
|
||||
```
|
||||
|
||||
2. Run: `python master_config_workflow_script.py`
|
||||
|
||||
**What Happens:**
|
||||
- Same workflow as metric changes
|
||||
- Plain text stored in config YAMLs
|
||||
- RST version generated and stored in docs/tools files
|
||||
|
||||
## Delta YAML Structure
|
||||
|
||||
### Complete Example
|
||||
|
||||
```yaml
|
||||
Addition:
|
||||
- Panel Config:
|
||||
id: 1100
|
||||
title: Compute Units - Compute Pipeline
|
||||
metric_tables:
|
||||
- metric_table:
|
||||
id: 1103
|
||||
title: Arithmetic Operations
|
||||
metrics:
|
||||
- F8 OPs:
|
||||
avg: AVG(((512 * SQ_INSTS_VALU_MFMA_MOPS_F8) / $denom))
|
||||
min: MIN(((512 * SQ_INSTS_VALU_MFMA_MOPS_F8) / $denom))
|
||||
max: MAX(((512 * SQ_INSTS_VALU_MFMA_MOPS_F8) / $denom))
|
||||
unit: (OPs + $normUnit)
|
||||
metric_descriptions:
|
||||
F8 OPs:
|
||||
plain: Number of 8-bit floating point operations
|
||||
rst: |-
|
||||
Number of 8-bit floating point operations per :ref:`normalization unit <normalization-units>`"
|
||||
|
||||
Deletion:
|
||||
- Panel Config:
|
||||
id: 1100
|
||||
title: Compute Units - Compute Pipeline
|
||||
metric_tables:
|
||||
- metric_table:
|
||||
id: 1103
|
||||
title: Arithmetic Operations
|
||||
metrics:
|
||||
- Old Metric:
|
||||
avg: AVG(something)
|
||||
metric_descriptions:
|
||||
Old Metric:
|
||||
plain: "Old description"
|
||||
|
||||
Modification:
|
||||
- Panel Config:
|
||||
id: 1100
|
||||
title: Compute Units - Compute Pipeline
|
||||
metric_tables:
|
||||
- metric_table:
|
||||
id: 1103
|
||||
title: Arithmetic Operations
|
||||
metrics:
|
||||
- Existing Metric:
|
||||
avg: AVG(new_formula) # Changed field only
|
||||
metric_descriptions:
|
||||
Existing Metric:
|
||||
plain: Updated description
|
||||
rst: >-
|
||||
Updated description with **RST**"
|
||||
```
|
||||
|
||||
### Rules for Deltas
|
||||
|
||||
1. **Must have all three sections**: Addition, Deletion, Modification (can be empty lists)
|
||||
2. **Metric descriptions**:
|
||||
- `plain` field is required
|
||||
- `rst` field is optional (defaults to copy of plain)
|
||||
3. **Delta filename**: Must be `<target_arch>_diff.yaml`
|
||||
4. **Location**: `src/rocprof_compute_soc/analysis_configs/gfx<arch>/config_delta/`
|
||||
|
||||
## Standalone Tool Usage
|
||||
|
||||
### Hash Manager
|
||||
|
||||
or:
|
||||
```bash
|
||||
# Compute hashes for all archs
|
||||
python hash_manager.py --compute-all src/rocprof_compute_soc/analysis_configs
|
||||
|
||||
# Detect changes
|
||||
python hash_manager.py --detect-changes src/rocprof_compute_soc/analysis_configs
|
||||
|
||||
# Update hashes for specific arch
|
||||
python hash_manager.py --update gfx950 src/rocprof_compute_soc/analysis_configs
|
||||
python tools/config_management/master_config_workflow_script.py --ci
|
||||
```
|
||||
|
||||
### Metric Description Manager
|
||||
This runs semantic hash validation only.
|
||||
|
||||
## Automated Testing
|
||||
### Pytest Hash Integrity Test
|
||||
|
||||
Located at:
|
||||
```bash
|
||||
# Sync descriptions for specific arch
|
||||
python metric_description_manager.py --sync-arch gfx950 src/rocprof_compute_soc/analysis_configs --latest-arch gfx950
|
||||
|
||||
# Sync all archs
|
||||
python metric_description_manager.py --sync-all src/rocprof_compute_soc/analysis_configs --latest-arch gfx950
|
||||
|
||||
# Validate descriptions
|
||||
python metric_description_manager.py --validate gfx950 src/rocprof_compute_soc/analysis_configs
|
||||
tests/test_autogen_config.py
|
||||
```
|
||||
|
||||
### Parse Config Template
|
||||
This test:
|
||||
|
||||
- parses `.config_hashes.json`
|
||||
- verifies **byte-for-byte** integrity of:
|
||||
- panel YAMLs
|
||||
- delta YAMLs
|
||||
- fails on:
|
||||
- missing files
|
||||
- changed content
|
||||
- stale hash DB
|
||||
Semantic correctness is enforced separately by `hash_checker.py`.
|
||||
|
||||
## Contributor Rules (Strict)
|
||||
|
||||
- Do **not** edit `.config_hashes.json` manually
|
||||
- Do **not** create multiple delta files per arch
|
||||
- Do **not** rename delta files arbitrarily
|
||||
- Do **not** regenerate full YAMLs unnecessarily
|
||||
- Use in-place edits (ruamel round-trip)
|
||||
- Use the master script for promotions
|
||||
- Expect CI to reject inconsistent states
|
||||
|
||||
## Summary
|
||||
|
||||
This system guarantees:
|
||||
|
||||
- A **single source of truth** for latest architecture
|
||||
- Deterministic, reviewable deltas
|
||||
- Stable diffs for Git review
|
||||
- Hash-backed immutability
|
||||
- Safe, transactional promotions
|
||||
- CI-enforced correctness
|
||||
|
||||
All correctness flows through:
|
||||
```bash
|
||||
# Generate template with metadata
|
||||
python parse_config_template.py src/rocprof_compute_soc/analysis_configs/gfx950 \
|
||||
tools/config_management/analysis_config_template.yaml \
|
||||
--latest-arch gfx950
|
||||
```
|
||||
|
||||
### Generate Delta
|
||||
|
||||
```bash
|
||||
# Generate delta from current arch to previous arch
|
||||
python generate_config_deltas.py \
|
||||
src/rocprof_compute_soc/analysis_configs/gfx950 \
|
||||
src/rocprof_compute_soc/analysis_configs/gfx940
|
||||
```
|
||||
|
||||
### Apply Delta
|
||||
|
||||
```bash
|
||||
# Apply delta to base arch
|
||||
python apply_config_deltas.py \
|
||||
src/rocprof_compute_soc/analysis_configs/gfx940 \
|
||||
src/rocprof_compute_soc/analysis_configs/gfx940/config_delta/gfx950_diff.yaml \
|
||||
output_dir
|
||||
```
|
||||
|
||||
### Verify Against Template
|
||||
|
||||
```bash
|
||||
# Validate all archs
|
||||
python verify_against_config_template.py \
|
||||
src/rocprof_compute_soc/analysis_configs \
|
||||
tools/config_management/analysis_config_template.yaml
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── src/rocprof_compute_soc/analysis_configs/
|
||||
│ ├── gfx940/
|
||||
│ │ ├── 0700_wavefront.yaml # Config with plain descriptions
|
||||
│ │ └── config_delta/
|
||||
│ │ └── gfx950_diff.yaml # Delta to apply changes
|
||||
│ ├── gfx941/
|
||||
│ └── gfx950/ # Latest arch
|
||||
│ ├── 0700_wavefront.yaml
|
||||
│ └── config_delta/
|
||||
│ └── gfx950_diff.yaml # Optional delta for modifications
|
||||
│
|
||||
├── tools/
|
||||
│ ├── config_management/
|
||||
│ │ ├── .config_hashes.json # Hash database (auto-generated)
|
||||
│ │ ├── analysis_config_template.yaml # Template with metadata
|
||||
│ │ ├── hash_manager.py
|
||||
│ │ ├── metric_description_manager.py
|
||||
│ │ ├── parse_config_template.py
|
||||
│ │ ├── generate_config_deltas.py
|
||||
│ │ ├── apply_config_deltas.py
|
||||
│ │ ├── verify_against_config_template.py
|
||||
│ │ ├── master_config_workflow_script.py
|
||||
│ │ └── config_workflow.yaml
|
||||
│ │
|
||||
│ └── per_arch_metric_definitions/
|
||||
│ ├── gfx940_metrics_description.yaml # RST only
|
||||
│ ├── gfx941_metrics_description.yaml
|
||||
│ └── gfx950_metrics_description.yaml
|
||||
│
|
||||
├── docs/data/
|
||||
│ └── metrics_description.yaml # RST only, latest arch only
|
||||
│
|
||||
└── .backups/ # Auto-generated backups
|
||||
└── 20250115_143022/ # Timestamped backup
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config_workflow.yaml` to customize paths and behavior:
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
template: tools/config_management/analysis_config_template.yaml
|
||||
configs_root: src/rocprof_compute_soc/analysis_configs
|
||||
backups: .backups
|
||||
hashes: tools/config_management/.config_hashes.json
|
||||
per_arch_metrics: tools/per_arch_metric_definitions
|
||||
docs_metrics: docs/data/metrics_description.yaml
|
||||
|
||||
validation:
|
||||
strict_mode: true # Fail on warnings
|
||||
verify_after_changes: true # Validate after operations
|
||||
|
||||
behavior:
|
||||
require_confirmation: true # Prompt before changes
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Validation Failures
|
||||
|
||||
If validation fails:
|
||||
1. All changes are automatically reverted
|
||||
2. Backup is restored
|
||||
3. Detailed error report is printed
|
||||
4. Fix the issue and run again
|
||||
|
||||
### Hash Mismatches
|
||||
|
||||
If hashes are out of sync:
|
||||
```bash
|
||||
# Recompute all hashes
|
||||
python hash_manager.py --compute-all src/rocprof_compute_soc/analysis_configs
|
||||
```
|
||||
|
||||
### Description Validation Errors
|
||||
|
||||
Common issues:
|
||||
- **Missing descriptions**: Warning only (won't fail)
|
||||
- **Invalid RST syntax**: Error (will fail and revert)
|
||||
- **Missing plain text**: Error (plain is required)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use master_config_workflow_script.py** - Don't run individual scripts manually unless debugging
|
||||
2. **Test with --dry-run first** - See what will happen before committing
|
||||
3. **Use deltas for complex changes** - Easier to review and version control
|
||||
4. **Keep descriptions updated** - Plain text in configs, RST in docs
|
||||
5. **One change at a time** - If multiple archs need updates, do them sequentially
|
||||
6. **Check validation output** - Review warnings even if they don't fail
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No changes detected"
|
||||
|
||||
- Check that files were actually modified
|
||||
- Ensure you're in the correct directory
|
||||
- Verify hash database exists: `tools/config_management/.config_hashes.json`
|
||||
|
||||
### "Validation failed"
|
||||
|
||||
- Review the error output carefully
|
||||
- Check that new metrics match template structure
|
||||
- Ensure panel IDs are correct
|
||||
- Verify data source ordering
|
||||
|
||||
### "Failed to sync metric descriptions"
|
||||
|
||||
- Check RST syntax in descriptions
|
||||
- Ensure all metrics have descriptions
|
||||
- Verify section_panel_map includes your table ID
|
||||
|
||||
### Changes not detected after manual edit
|
||||
|
||||
```bash
|
||||
# Force recompute hashes
|
||||
python hash_manager.py --compute-all src/rocprof_compute_soc/analysis_configs
|
||||
|
||||
# Then run workflow
|
||||
python master_config_workflow_script.py
|
||||
```
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Adding New Architecture Support
|
||||
|
||||
When adding a completely new architecture line:
|
||||
|
||||
1. Ensure table IDs are in `metric_description_manager.py`'s `SECTION_PANEL_MAP`
|
||||
2. Follow existing naming conventions (gfxXXX)
|
||||
3. Create complete YAML set (don't start with partial configs)
|
||||
|
||||
### Modifying the Workflow
|
||||
|
||||
If you need to modify the workflow behavior:
|
||||
|
||||
1. Edit `config_workflow.yaml` for path/behavior changes
|
||||
2. Edit `master_config_workflow_script.py` for workflow logic changes
|
||||
3. Test with `--dry-run` extensively
|
||||
4. Update this README
|
||||
|
||||
|
||||
# Pre-commit: Hash Consistency Check
|
||||
|
||||
We ship a lightweight pre-commit hook that catches inconsistent hash updates across config YAMLs and deltas.
|
||||
|
||||
## What it enforces (per arch)
|
||||
|
||||
* Latest panels changed → latest delta must change (if there are older archs).
|
||||
* Latest delta changed → latest panels must change or a new arch must be added.
|
||||
* Older arch panels changed → that arch’s delta must change.
|
||||
* Older arch delta changed → either latest panels or that arch’s panels must have changed.
|
||||
|
||||
## Setup
|
||||
|
||||
Install and enable pre-commit:
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
Our .pre-commit-config.yaml includes a local hook that runs the checker.
|
||||
|
||||
```yaml
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: hash-check
|
||||
name: Hash consistency check
|
||||
entry: bash -lc 'cd projects/rocprofiler-compute && python3 tools/config_management/hash_checker.py'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
```
|
||||
|
||||
## Run manually
|
||||
|
||||
```bash
|
||||
# from super-repo root
|
||||
pre-commit run --all-files
|
||||
|
||||
# or directly in the subproject
|
||||
cd projects/rocprofiler-compute
|
||||
python3 tools/config_management/hash_checker.py
|
||||
master_config_workflow_script.py
|
||||
```
|
||||
|
||||
@@ -36,24 +36,14 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
try:
|
||||
from . import utils as cm_utils
|
||||
except Exception:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
if str(repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(repo_root))
|
||||
try:
|
||||
import config_management.utils as cm_utils # type: ignore
|
||||
except Exception:
|
||||
import utils as cm_utils # type: ignore
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
AUTOGEN_TEXT = (
|
||||
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
|
||||
"Generated by tools/config_management/apply_config_deltas.py\n"
|
||||
)
|
||||
from config_management import utils_ruamel as cm_utils # noqa: E402
|
||||
|
||||
|
||||
def find_table_in_config(config: dict, table_id: Any) -> Optional[dict]:
|
||||
def find_table(config: dict, table_id: Any) -> Optional[dict]:
|
||||
"""Find and return the table with given id, or None."""
|
||||
for item in config.get("Panel Config", {}).get("data source", []):
|
||||
table = item.get("metric_table")
|
||||
@@ -72,8 +62,8 @@ def add_table(config: dict, metric_table: dict) -> None:
|
||||
|
||||
def add_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
|
||||
"""Add metrics to existing table."""
|
||||
table = find_table_in_config(config, table_id)
|
||||
if not table:
|
||||
table = find_table(config, table_id)
|
||||
if table is None:
|
||||
print(f"WARNING: Table {table_id} not found for metric addition")
|
||||
return
|
||||
|
||||
@@ -90,7 +80,7 @@ def delete_table(config: dict, table_id: Any) -> None:
|
||||
for idx, item in enumerate(list(data_source)):
|
||||
table = item.get("metric_table")
|
||||
if isinstance(table, dict) and table.get("id") == table_id:
|
||||
data_source.pop(idx)
|
||||
del data_source[idx]
|
||||
print(f"Deleted table: {table_id}")
|
||||
return
|
||||
print(f"WARNING: Table {table_id} not found for deletion")
|
||||
@@ -98,8 +88,8 @@ def delete_table(config: dict, table_id: Any) -> None:
|
||||
|
||||
def delete_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
|
||||
"""Remove specific metrics from table."""
|
||||
table = find_table_in_config(config, table_id)
|
||||
if not table or "metric" not in table:
|
||||
table = find_table(config, table_id)
|
||||
if table is None or "metric" not in table:
|
||||
print(f"WARNING: Table {table_id} not found or has no metrics")
|
||||
return
|
||||
|
||||
@@ -112,8 +102,8 @@ def delete_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
|
||||
|
||||
def modify_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
|
||||
"""Modify specific fields in existing metrics."""
|
||||
table = find_table_in_config(config, table_id)
|
||||
if not table or "metric" not in table:
|
||||
table = find_table(config, table_id)
|
||||
if table is None or "metric" not in table:
|
||||
print(f"WARNING: Table {table_id} not found or has no metrics")
|
||||
return
|
||||
|
||||
@@ -129,19 +119,17 @@ def modify_metrics(config: dict, table_id: Any, metrics: list[dict]) -> None:
|
||||
|
||||
def add_descriptions(config: dict, descriptions: dict) -> None:
|
||||
"""Add metric descriptions to config."""
|
||||
pc = config.setdefault("Panel Config", {})
|
||||
pc.setdefault("metrics_description", {})
|
||||
md = pc["metrics_description"]
|
||||
md = config["Panel Config"].setdefault("metrics_description", {})
|
||||
|
||||
for metric_name, desc_data in descriptions.items():
|
||||
value = desc_data if isinstance(desc_data, dict) else desc_data
|
||||
md[metric_name] = value
|
||||
md[metric_name] = dict(desc_data) if isinstance(desc_data, dict) else desc_data
|
||||
|
||||
print(f"Added description: {metric_name}")
|
||||
|
||||
|
||||
def delete_descriptions(config: dict, descriptions: dict) -> None:
|
||||
"""Remove metric descriptions from config."""
|
||||
md = config.get("Panel Config", {}).get("metrics_description", {})
|
||||
md = config["Panel Config"].setdefault("metrics_description", {})
|
||||
for metric_name in descriptions.keys():
|
||||
if metric_name in md:
|
||||
del md[metric_name]
|
||||
@@ -150,21 +138,25 @@ def delete_descriptions(config: dict, descriptions: dict) -> None:
|
||||
|
||||
def modify_descriptions(config: dict, descriptions: dict) -> None:
|
||||
"""Modify metric descriptions in config."""
|
||||
pc = config.setdefault("Panel Config", {})
|
||||
pc.setdefault("metrics_description", {})
|
||||
md = pc["metrics_description"]
|
||||
md = config["Panel Config"].setdefault("metrics_description", {})
|
||||
|
||||
for metric_name, desc_data in descriptions.items():
|
||||
value = desc_data if isinstance(desc_data, dict) else desc_data
|
||||
md[metric_name] = value
|
||||
if isinstance(desc_data, dict):
|
||||
new_dict = {}
|
||||
for k, v in desc_data.items():
|
||||
new_dict[k] = v
|
||||
md[metric_name] = new_dict
|
||||
else:
|
||||
md[metric_name] = desc_data
|
||||
|
||||
print(f"Added description: {metric_name}")
|
||||
|
||||
|
||||
def apply_changes(config: dict, changes: list[dict], category: str) -> None:
|
||||
"""Apply delta changes to configuration."""
|
||||
for change in changes:
|
||||
for mt_wrapper in change.get("metric_tables", []):
|
||||
mt = mt_wrapper.get("metric_table", mt_wrapper)
|
||||
mt = change.get("metric_table")
|
||||
if mt:
|
||||
table_id = mt.get("id")
|
||||
|
||||
if category == "Addition":
|
||||
@@ -199,7 +191,7 @@ def apply_delta(
|
||||
output_dir: Union[str, Path],
|
||||
) -> None:
|
||||
"""Apply delta YAML to all files in base directory."""
|
||||
delta = cm_utils.load_yaml(delta_file)
|
||||
delta = cm_utils.load_yaml(delta_file, round_trip=True)
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -214,7 +206,7 @@ def apply_delta(
|
||||
|
||||
base_path = Path(base_dir)
|
||||
for yaml_file in base_path.glob("*.yaml"):
|
||||
config = cm_utils.load_yaml(yaml_file)
|
||||
config = cm_utils.load_yaml(yaml_file, round_trip=True)
|
||||
panel_id = config.get("Panel Config", {}).get("id")
|
||||
|
||||
if panel_id in changes_by_panel:
|
||||
@@ -226,7 +218,8 @@ def apply_delta(
|
||||
config, changes_by_panel[panel_id][category], category
|
||||
)
|
||||
|
||||
cm_utils.save_yaml(config, output_path / yaml_file.name, AUTOGEN_TEXT)
|
||||
cm_utils.strip_existing_header(config)
|
||||
cm_utils.save_yaml(config, output_path / yaml_file.name)
|
||||
print(f"Saved: {yaml_file.name}")
|
||||
else:
|
||||
shutil.copy(yaml_file, output_path / yaml_file.name)
|
||||
|
||||
@@ -11,7 +11,7 @@ paths:
|
||||
backups: .backups
|
||||
|
||||
# Hash database file
|
||||
hashes: tools/config_management/.config_hashes.json
|
||||
hashes: src/utils/.config_hashes.json
|
||||
|
||||
# Per-arch metric definitions output
|
||||
per_arch_metrics: tools/per_arch_metric_definitions
|
||||
|
||||
+218
-279
@@ -34,326 +34,265 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
try:
|
||||
from . import utils as cm_utils
|
||||
except Exception:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
if str(repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(repo_root))
|
||||
try:
|
||||
import config_management.utils as cm_utils # type: ignore
|
||||
except Exception:
|
||||
import utils as cm_utils # type: ignore
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
AUTOGEN_TEXT = (
|
||||
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
|
||||
"Generated by tools/config_management/generate_config_deltas.py\n"
|
||||
)
|
||||
from config_management import utils_ruamel as cm_utils # noqa: E402
|
||||
from ruamel.yaml.comments import CommentedMap # noqa: E402
|
||||
|
||||
|
||||
def get_metric_tables(data: dict) -> list[dict]:
|
||||
"""Extract all metric tables from data source."""
|
||||
tables: list[dict] = []
|
||||
for item in data.get("Panel Config", {}).get("data source", []):
|
||||
mt = item.get("metric_table")
|
||||
if isinstance(mt, dict):
|
||||
tables.append(mt)
|
||||
return tables
|
||||
def load_yaml_roundtrip(path: Path) -> Any:
|
||||
return cm_utils.load_yaml(path, round_trip=True)
|
||||
|
||||
|
||||
def get_metric_descriptions(data: dict) -> dict:
|
||||
"""Extract metric descriptions from panel config."""
|
||||
return data.get("Panel Config", {}).get("metrics_description", {}) or {}
|
||||
def diff_metric_fields(base_fields, new_fields) -> Optional[CommentedMap]:
|
||||
out = CommentedMap()
|
||||
|
||||
for key in new_fields:
|
||||
if key not in base_fields or base_fields[key] != new_fields[key]:
|
||||
# Preserve the original value with comments
|
||||
out[key] = new_fields[key]
|
||||
|
||||
return out if out else None
|
||||
|
||||
|
||||
def compare_metrics(
|
||||
prev_metrics: dict, curr_metrics: dict
|
||||
def descriptions_equal(base_desc, new_desc) -> bool:
|
||||
"""Check if two descriptions are equal by comparing their string representation."""
|
||||
return str(base_desc) == str(new_desc)
|
||||
|
||||
|
||||
def diff_metric_table(
|
||||
base_table, new_table
|
||||
) -> tuple[list[dict], list[dict], list[dict]]:
|
||||
"""Compare metrics and return (additions, deletions, modifications)."""
|
||||
prev_keys = set(prev_metrics.keys())
|
||||
curr_keys = set(curr_metrics.keys())
|
||||
|
||||
additions = [{name: curr_metrics[name]} for name in sorted(curr_keys - prev_keys)]
|
||||
deletions = [{name: prev_metrics[name]} for name in sorted(prev_keys - curr_keys)]
|
||||
|
||||
modifications: list[dict] = []
|
||||
for name in sorted(prev_keys & curr_keys):
|
||||
if prev_metrics[name] != curr_metrics[name]:
|
||||
all_fields = set(prev_metrics[name].keys()) | set(curr_metrics[name].keys())
|
||||
modified_fields = {
|
||||
field: curr_metrics[name].get(field)
|
||||
for field in all_fields
|
||||
if prev_metrics[name].get(field) != curr_metrics[name].get(field)
|
||||
}
|
||||
if modified_fields:
|
||||
modifications.append({name: modified_fields})
|
||||
|
||||
return additions, deletions, modifications
|
||||
|
||||
|
||||
def compare_descriptions(
|
||||
prev_descriptions: dict, curr_descriptions: dict
|
||||
) -> tuple[dict, dict, dict]:
|
||||
"""
|
||||
Compare metric descriptions and return (additions, deletions, modifications).
|
||||
Values are dicts with 'plain' and 'rst'.
|
||||
Returns (additions, modifications, deletions) tuple.
|
||||
"""
|
||||
prev_keys = set(prev_descriptions.keys())
|
||||
curr_keys = set(curr_descriptions.keys())
|
||||
addition_metrics: list[dict] = []
|
||||
modification_metrics: list[dict] = []
|
||||
deletion_metrics: list[dict] = []
|
||||
|
||||
additions: dict = {}
|
||||
deletions: dict = {}
|
||||
modifications: dict = {}
|
||||
base_metrics = base_table.get("metric", {})
|
||||
new_metrics = new_table.get("metric", {})
|
||||
|
||||
for name in sorted(curr_keys - prev_keys):
|
||||
desc = curr_descriptions[name]
|
||||
additions[name] = (
|
||||
desc if isinstance(desc, dict) else {"plain": desc, "rst": desc}
|
||||
)
|
||||
# Metrics deleted
|
||||
for metric in base_metrics:
|
||||
if metric not in new_metrics:
|
||||
deletion_metrics.append({metric: None})
|
||||
|
||||
for name in sorted(prev_keys - curr_keys):
|
||||
desc = prev_descriptions[name]
|
||||
deletions[name] = (
|
||||
desc if isinstance(desc, dict) else {"plain": desc, "rst": desc}
|
||||
)
|
||||
# Metrics added or modified
|
||||
for metric in new_metrics:
|
||||
if metric not in base_metrics:
|
||||
# Entire metric is new - preserve original with comments
|
||||
addition_metrics.append({metric: new_metrics[metric]})
|
||||
else:
|
||||
# Field-level diff
|
||||
changes = diff_metric_fields(base_metrics[metric], new_metrics[metric])
|
||||
if changes:
|
||||
modification_metrics.append({metric: changes})
|
||||
|
||||
for name in sorted(prev_keys & curr_keys):
|
||||
prev_desc = prev_descriptions[name]
|
||||
curr_desc = curr_descriptions[name]
|
||||
|
||||
prev_plain = (
|
||||
prev_desc if isinstance(prev_desc, str) else prev_desc.get("plain", "")
|
||||
)
|
||||
curr_plain = (
|
||||
curr_desc if isinstance(curr_desc, str) else curr_desc.get("plain", "")
|
||||
)
|
||||
|
||||
prev_rst = (
|
||||
prev_desc
|
||||
if isinstance(prev_desc, str)
|
||||
else prev_desc.get("rst", prev_plain)
|
||||
)
|
||||
curr_rst = (
|
||||
curr_desc
|
||||
if isinstance(curr_desc, str)
|
||||
else curr_desc.get("rst", curr_plain)
|
||||
)
|
||||
|
||||
if prev_plain != curr_plain or prev_rst != curr_rst:
|
||||
modifications[name] = {"plain": curr_plain, "rst": curr_rst}
|
||||
|
||||
return additions, deletions, modifications
|
||||
return addition_metrics, modification_metrics, deletion_metrics
|
||||
|
||||
|
||||
def compare_tables(
|
||||
prev_tables: list[dict], curr_tables: list[dict]
|
||||
) -> tuple[list[dict], list[dict], list[dict]]:
|
||||
"""Compare tables and return (additions, deletions, modifications)."""
|
||||
prev_dict = {t["id"]: t for t in prev_tables}
|
||||
curr_dict = {t["id"]: t for t in curr_tables}
|
||||
def diff_descriptions(
|
||||
base_md, new_md
|
||||
) -> tuple[Optional[CommentedMap], Optional[CommentedMap], Optional[CommentedMap]]:
|
||||
"""
|
||||
Returns (additions, modifications, deletions) tuple.
|
||||
"""
|
||||
additions = CommentedMap()
|
||||
modifications = CommentedMap()
|
||||
deletions = CommentedMap()
|
||||
|
||||
prev_ids = set(prev_dict.keys())
|
||||
curr_ids = set(curr_dict.keys())
|
||||
# Deletions
|
||||
for key in base_md:
|
||||
if key not in new_md:
|
||||
deletions[key] = None
|
||||
|
||||
additions: list[dict] = []
|
||||
deletions: list[dict] = []
|
||||
modifications: list[dict] = []
|
||||
# Additions and modifications
|
||||
for key in new_md:
|
||||
if key not in base_md:
|
||||
# New description - preserve original node
|
||||
additions[key] = new_md[key]
|
||||
else:
|
||||
# Check if modified
|
||||
if not descriptions_equal(base_md[key], new_md[key]):
|
||||
# Preserve original node to maintain style
|
||||
modifications[key] = new_md[key]
|
||||
|
||||
additions.extend(curr_dict[tid] for tid in sorted(curr_ids - prev_ids))
|
||||
deletions.extend(prev_dict[tid] for tid in sorted(prev_ids - curr_ids))
|
||||
return (
|
||||
additions if additions else None,
|
||||
modifications if modifications else None,
|
||||
deletions if deletions else None,
|
||||
)
|
||||
|
||||
for tid in sorted(prev_ids & curr_ids):
|
||||
prev_metrics = prev_dict[tid].get("metric", {}) or {}
|
||||
curr_metrics = curr_dict[tid].get("metric", {}) or {}
|
||||
|
||||
metric_adds, metric_dels, metric_mods = compare_metrics(
|
||||
prev_metrics, curr_metrics
|
||||
)
|
||||
def extract_metric_tables(data_sources) -> list[Any]:
|
||||
out = []
|
||||
for ds in data_sources:
|
||||
if "metric_table" in ds:
|
||||
mt = ds["metric_table"]
|
||||
table_id = mt.get("id")
|
||||
if table_id is not None:
|
||||
out.append((table_id, mt))
|
||||
return out
|
||||
|
||||
if metric_adds:
|
||||
additions.append({
|
||||
"id": tid,
|
||||
"title": curr_dict[tid].get("title"),
|
||||
"metrics": metric_adds,
|
||||
})
|
||||
if metric_dels:
|
||||
deletions.append({
|
||||
"id": tid,
|
||||
"title": prev_dict[tid].get("title"),
|
||||
"metrics": metric_dels,
|
||||
})
|
||||
if metric_mods:
|
||||
modifications.append({
|
||||
"id": tid,
|
||||
"title": curr_dict[tid].get("title"),
|
||||
"metrics": metric_mods,
|
||||
|
||||
def diff_panel(base_config, new_config) -> Optional[dict[str, list[Any]]]:
|
||||
"""
|
||||
Produce delta for a single panel.
|
||||
Returns dicts under keys:
|
||||
'Addition', 'Deletion', 'Modification'
|
||||
or None if no diffs.
|
||||
"""
|
||||
out = {"Addition": [], "Deletion": [], "Modification": []}
|
||||
panel_id = base_config["Panel Config"]["id"]
|
||||
|
||||
# Table-level diffs
|
||||
base_tables = extract_metric_tables(
|
||||
base_config["Panel Config"].get("data source", [])
|
||||
)
|
||||
new_tables = extract_metric_tables(
|
||||
new_config["Panel Config"].get("data source", [])
|
||||
)
|
||||
|
||||
# Indexing by table ID to preserve order
|
||||
base_by_id = {tid: table for (tid, table) in base_tables}
|
||||
new_by_id = {tid: table for (tid, table) in new_tables}
|
||||
|
||||
# Table deletions
|
||||
for tid in base_by_id:
|
||||
if tid not in new_by_id:
|
||||
out["Deletion"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_tables": [{"metric_table": {"id": tid}}],
|
||||
})
|
||||
|
||||
return additions, deletions, modifications
|
||||
|
||||
|
||||
def format_metric_fields(metric_data: dict) -> list[str]:
|
||||
"""Format metric fields as YAML lines."""
|
||||
lines: list[str] = []
|
||||
for field_name, field_value in metric_data.items():
|
||||
if isinstance(field_value, str) and (
|
||||
"\n" in field_value or len(field_value) > 80
|
||||
):
|
||||
lines.append(f" {field_name}: |")
|
||||
lines.extend(
|
||||
f" {line}" for line in field_value.split("\n")
|
||||
# Table additions + modifications
|
||||
for tid in new_by_id:
|
||||
if tid not in base_by_id:
|
||||
# Entire table is added - preserve original
|
||||
out["Addition"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_tables": [{"metric_table": new_by_id[tid]}],
|
||||
})
|
||||
else:
|
||||
# Check metric-level diffs
|
||||
additions, modifications, deletions = diff_metric_table(
|
||||
base_by_id[tid], new_by_id[tid]
|
||||
)
|
||||
else:
|
||||
lines.append(f" {field_name}: {field_value}")
|
||||
return lines
|
||||
|
||||
if deletions:
|
||||
out["Deletion"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_table": {"id": tid, "metrics": deletions},
|
||||
})
|
||||
|
||||
if additions:
|
||||
out["Addition"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_table": {"id": tid, "metrics": additions},
|
||||
})
|
||||
|
||||
if modifications:
|
||||
out["Modification"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_table": {"id": tid, "metrics": modifications},
|
||||
})
|
||||
|
||||
# Description diffs
|
||||
base_md = base_config["Panel Config"].get("metrics_description", {})
|
||||
new_md = new_config["Panel Config"].get("metrics_description", {})
|
||||
desc_additions, desc_modifications, desc_deletions = diff_descriptions(
|
||||
base_md, new_md
|
||||
)
|
||||
|
||||
if desc_deletions:
|
||||
out["Deletion"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_descriptions": desc_deletions,
|
||||
})
|
||||
|
||||
if desc_additions:
|
||||
out["Addition"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_descriptions": desc_additions,
|
||||
})
|
||||
|
||||
if desc_modifications:
|
||||
out["Modification"].append({
|
||||
"Panel Config": {"id": panel_id},
|
||||
"metric_descriptions": desc_modifications,
|
||||
})
|
||||
|
||||
# Clean empties
|
||||
if not out["Addition"]:
|
||||
del out["Addition"]
|
||||
if not out["Deletion"]:
|
||||
del out["Deletion"]
|
||||
if not out["Modification"]:
|
||||
del out["Modification"]
|
||||
|
||||
return out if out else None
|
||||
|
||||
|
||||
def format_description_fields(desc_data: dict) -> list[str]:
|
||||
"""Format description fields as YAML lines."""
|
||||
lines: list[str] = []
|
||||
for field_name, field_value in desc_data.items():
|
||||
if isinstance(field_value, str) and (
|
||||
"\n" in field_value or len(field_value) > 80
|
||||
):
|
||||
lines.append(f" {field_name}: |")
|
||||
lines.extend(f" {line}" for line in field_value.split("\n"))
|
||||
else:
|
||||
lines.append(f" {field_name}: {field_value}")
|
||||
return lines
|
||||
def generate_arch_delta(base_dir: Path, new_dir: Path) -> CommentedMap:
|
||||
"""
|
||||
Compare all YAML files panel-by-panel.
|
||||
"""
|
||||
out = CommentedMap()
|
||||
out["Addition"] = []
|
||||
out["Deletion"] = []
|
||||
out["Modification"] = []
|
||||
|
||||
|
||||
def format_output(combined_diff: dict) -> str:
|
||||
"""Format the diff dictionary into a YAML string."""
|
||||
lines: list[str] = []
|
||||
for category in ("Addition", "Deletion", "Modification"):
|
||||
lines.append(f"{category}:")
|
||||
if not combined_diff.get(category):
|
||||
lines.append(" []")
|
||||
lines.append("")
|
||||
base_files = sorted(base_dir.glob("*.yaml"))
|
||||
for base_file in base_files:
|
||||
new_file = new_dir / base_file.name
|
||||
if not new_file.exists():
|
||||
continue
|
||||
|
||||
for panel_item in combined_diff[category]:
|
||||
pc = panel_item["panel_config"]
|
||||
lines.extend([
|
||||
" - Panel Config:",
|
||||
f" id: {pc['id']}",
|
||||
f" title: {pc['title']}",
|
||||
])
|
||||
base_config = load_yaml_roundtrip(base_file)
|
||||
new_config = load_yaml_roundtrip(new_file)
|
||||
|
||||
if panel_item.get("metric_tables"):
|
||||
lines.append(" metric_tables:")
|
||||
for mt in panel_item["metric_tables"]:
|
||||
lines.extend([
|
||||
" - metric_table:",
|
||||
f" id: {mt['id']}",
|
||||
f" title: {mt['title']}",
|
||||
" metrics:",
|
||||
])
|
||||
metrics_to_format = mt.get("metrics") or [
|
||||
{name: data} for name, data in (mt.get("metric") or {}).items()
|
||||
]
|
||||
for metric in metrics_to_format:
|
||||
for metric_name, metric_data in metric.items():
|
||||
lines.append(f" - {metric_name}:")
|
||||
lines.extend(format_metric_fields(metric_data))
|
||||
diff = diff_panel(base_config, new_config)
|
||||
if not diff:
|
||||
continue
|
||||
|
||||
if panel_item.get("metric_descriptions"):
|
||||
lines.append(" metric_descriptions:")
|
||||
for metric_name, desc_data in panel_item["metric_descriptions"].items():
|
||||
lines.append(f" {metric_name}:")
|
||||
lines.extend(format_description_fields(desc_data))
|
||||
if "Addition" in diff:
|
||||
out["Addition"].extend(diff["Addition"])
|
||||
if "Deletion" in diff:
|
||||
out["Deletion"].extend(diff["Deletion"])
|
||||
if "Modification" in diff:
|
||||
out["Modification"].extend(diff["Modification"])
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
# Strip empty categories
|
||||
if not out["Addition"]:
|
||||
del out["Addition"]
|
||||
if not out["Deletion"]:
|
||||
del out["Deletion"]
|
||||
if not out["Modification"]:
|
||||
del out["Modification"]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python generate_config_deltas.py <curr_arch_dir> <prev_arch_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
curr_arch_dir = Path(sys.argv[1])
|
||||
prev_arch_dir = Path(sys.argv[2])
|
||||
|
||||
if not curr_arch_dir.is_dir() or not prev_arch_dir.is_dir():
|
||||
print("Error: Both arguments must be directories")
|
||||
sys.exit(1)
|
||||
|
||||
curr_files = {f.name for f in curr_arch_dir.glob("*.yaml")}
|
||||
prev_files = {f.name for f in prev_arch_dir.glob("*.yaml")}
|
||||
common_files = curr_files & prev_files
|
||||
|
||||
if not common_files:
|
||||
print("Error: No common YAML files found")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Comparing {len(common_files)} files...")
|
||||
|
||||
combined_diff = {"Addition": [], "Deletion": [], "Modification": []}
|
||||
|
||||
for filename in sorted(common_files):
|
||||
curr_data = cm_utils.load_yaml(curr_arch_dir / filename)
|
||||
prev_data = cm_utils.load_yaml(prev_arch_dir / filename)
|
||||
|
||||
curr_pc = curr_data.get("Panel Config", {}) or {}
|
||||
prev_pc = prev_data.get("Panel Config", {}) or {}
|
||||
|
||||
curr_tables = get_metric_tables(curr_data)
|
||||
prev_tables = get_metric_tables(prev_data)
|
||||
|
||||
curr_descriptions = get_metric_descriptions(curr_data)
|
||||
prev_descriptions = get_metric_descriptions(prev_data)
|
||||
|
||||
table_adds, table_dels, table_mods = compare_tables(prev_tables, curr_tables)
|
||||
desc_adds, desc_dels, desc_mods = compare_descriptions(
|
||||
prev_descriptions, curr_descriptions
|
||||
if len(sys.argv) != 4:
|
||||
print(
|
||||
"Usage: python generate_config_deltas.py <base_arch_dir> <new_arch_dir> <output_delta_yaml>" # noqa: E501
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if table_adds or desc_adds:
|
||||
entry = {
|
||||
"panel_config": {"id": curr_pc.get("id"), "title": curr_pc.get("title")}
|
||||
}
|
||||
if table_adds:
|
||||
entry["metric_tables"] = table_adds
|
||||
if desc_adds:
|
||||
entry["metric_descriptions"] = desc_adds
|
||||
combined_diff["Addition"].append(entry)
|
||||
base_dir = Path(sys.argv[1])
|
||||
new_dir = Path(sys.argv[2])
|
||||
out_file = Path(sys.argv[3])
|
||||
|
||||
if table_dels or desc_dels:
|
||||
entry = {
|
||||
"panel_config": {"id": prev_pc.get("id"), "title": prev_pc.get("title")}
|
||||
}
|
||||
if table_dels:
|
||||
entry["metric_tables"] = table_dels
|
||||
if desc_dels:
|
||||
entry["metric_descriptions"] = desc_dels
|
||||
combined_diff["Deletion"].append(entry)
|
||||
delta = generate_arch_delta(base_dir, new_dir)
|
||||
|
||||
if table_mods or desc_mods:
|
||||
entry = {
|
||||
"panel_config": {"id": curr_pc.get("id"), "title": curr_pc.get("title")}
|
||||
}
|
||||
if table_mods:
|
||||
entry["metric_tables"] = table_mods
|
||||
if desc_mods:
|
||||
entry["metric_descriptions"] = desc_mods
|
||||
combined_diff["Modification"].append(entry)
|
||||
|
||||
output = AUTOGEN_TEXT + format_output(combined_diff)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("COMBINED DIFF OUTPUT:")
|
||||
print("=" * 80)
|
||||
print(output)
|
||||
|
||||
output_dir = prev_arch_dir / "config_delta"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
output_file = output_dir / f"{curr_arch_dir.name}_diff.yaml"
|
||||
with open(output_file, "w") as f:
|
||||
f.write(output)
|
||||
|
||||
print(f"\nDiff written to: {output_file}")
|
||||
cm_utils.save_yaml(delta, out_file)
|
||||
print(f"Delta generated at: {out_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -43,7 +43,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
DEFAULT_HASH_DB = "tools/config_management/.config_hashes.json"
|
||||
DEFAULT_HASH_DB = "src/utils/.config_hashes.json"
|
||||
|
||||
|
||||
def compute_file_hash(filepath: Path) -> str:
|
||||
|
||||
+221
-950
File diff ditekan karena terlalu besar
Load Diff
+6
-16
@@ -43,21 +43,11 @@ from typing import Union
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from . import utils as cm_utils
|
||||
except Exception:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
if str(repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(repo_root))
|
||||
try:
|
||||
import config_management.utils as cm_utils # type: ignore
|
||||
except Exception:
|
||||
import utils as cm_utils # type: ignore
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
AUTOGEN_TEXT = (
|
||||
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
|
||||
"Generated by tools/config_management/metric_description_manager.py\n"
|
||||
)
|
||||
from config_management import utils_ruamel as cm_utils # noqa: E402
|
||||
|
||||
# Section to panel ID mapping for organizing descriptions
|
||||
SECTION_PANEL_MAP: dict[str, int] = {
|
||||
@@ -274,7 +264,7 @@ def update_per_arch_metrics_file(
|
||||
entry["unit"] = desc_data["unit"]
|
||||
rst_descriptions[section][metric_name] = entry
|
||||
|
||||
cm_utils.save_yaml(rst_descriptions, output_path, AUTOGEN_TEXT)
|
||||
cm_utils.save_yaml(rst_descriptions, output_path)
|
||||
print(f"Updated: {output_path}")
|
||||
|
||||
|
||||
@@ -303,7 +293,7 @@ def update_docs_metrics_file(
|
||||
|
||||
docs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cm_utils.save_yaml(existing, docs_path, AUTOGEN_TEXT)
|
||||
cm_utils.save_yaml(existing, docs_path)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
##############################################################################
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Parse panel configuration based on YAML files for an architecture.
|
||||
Usage:
|
||||
python parse_config_template.py <dir_path> [output_file.yaml] [--latest-arch ARCH]
|
||||
parse_config_template.py
|
||||
|
||||
Parse panel configuration based on YAML files for an architecture and, optionally,
|
||||
generate a lightweight template describing panel IDs, titles, aliases, and
|
||||
data-source ordering.
|
||||
|
||||
Usage
|
||||
-----
|
||||
Generate a template from an architecture directory:
|
||||
|
||||
python tools/config_management/parse_config_template.py \
|
||||
analysis_configs/gfx950 \
|
||||
analysis_configs/config_template.yaml \
|
||||
--latest-arch gfx950
|
||||
|
||||
Inspect an architecture (no template written):
|
||||
|
||||
python tools/config_management/parse_config_template.py \
|
||||
analysis_configs/gfx950
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,45 +53,93 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
try:
|
||||
from . import utils as cm_utils
|
||||
except Exception:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
if str(repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(repo_root))
|
||||
try:
|
||||
import config_management.utils as cm_utils # type: ignore
|
||||
except Exception:
|
||||
import utils as cm_utils # type: ignore
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
AUTOGEN_TEXT = (
|
||||
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
|
||||
"Generated by tools/config_management/parse_config_template.py\n"
|
||||
)
|
||||
from config_management import utils_ruamel as cm_utils # noqa: E402
|
||||
|
||||
|
||||
def parse_panel_config(yaml_file: Path) -> Optional[dict]:
|
||||
"""Parse a single YAML file and extract panel and data source info."""
|
||||
def normalize_panel_id(panel_id: Optional[int]) -> Optional[int]:
|
||||
"""Normalize panel ID by dividing by 100 if needed."""
|
||||
if panel_id is None:
|
||||
return None
|
||||
return panel_id // 100 if panel_id >= 100 else panel_id
|
||||
|
||||
|
||||
def normalize_table_id(table_id: Optional[int]) -> Optional[int]:
|
||||
"""Normalize table ID using modulo 100."""
|
||||
if table_id is None:
|
||||
return None
|
||||
return table_id % 100
|
||||
|
||||
|
||||
def parse_panel_config(yaml_file: Path) -> Optional[dict[str, Any]]:
|
||||
"""
|
||||
Parse a single panel YAML file and extract template-relevant info.
|
||||
|
||||
Returns a dict with:
|
||||
- file: panel filename (without leading numeric prefix)
|
||||
- panel_id: normalized panel id (id // 100 when >= 100)
|
||||
- panel_title: Panel Config.title
|
||||
- panel_alias: Panel Config.alias (optional)
|
||||
- data_sources: ordered list of
|
||||
{type: <key>, id: <normalized table id>, title: <title>}
|
||||
or None if the file does not contain a valid Panel Config or fails basic checks.
|
||||
"""
|
||||
data = cm_utils.load_yaml(yaml_file)
|
||||
panel_config = data.get("Panel Config")
|
||||
if not isinstance(panel_config, dict):
|
||||
print(f"WARNING: {yaml_file} has no valid 'Panel Config' mapping, skipping.")
|
||||
return None
|
||||
|
||||
# Enforce presence of core panel-level keys
|
||||
missing_keys: list[str] = []
|
||||
for key in ("id", "title", "data source", "metrics_description"):
|
||||
if key not in panel_config:
|
||||
missing_keys.append(key)
|
||||
|
||||
if missing_keys:
|
||||
missing_str = ", ".join(missing_keys)
|
||||
print(
|
||||
f"ERROR: {yaml_file} is missing required Panel Config keys: {missing_str}"
|
||||
)
|
||||
return None
|
||||
|
||||
filename = (
|
||||
yaml_file.name.split("_", 1)[1] if "_" in yaml_file.name else yaml_file.name
|
||||
)
|
||||
|
||||
panel_id = panel_config.get("id")
|
||||
if panel_id and panel_id >= 100:
|
||||
panel_id = panel_id // 100
|
||||
raw_panel_id = panel_config.get("id")
|
||||
if not isinstance(raw_panel_id, int):
|
||||
print(
|
||||
f"ERROR: {yaml_file} has non-integer or missing Panel Config.id "
|
||||
f"({raw_panel_id!r})"
|
||||
)
|
||||
return None
|
||||
|
||||
data_sources = []
|
||||
for ds in panel_config.get("data source", []):
|
||||
panel_id = normalize_panel_id(raw_panel_id)
|
||||
|
||||
# Extract and normalize data sources
|
||||
data_sources: list[dict[str, Any]] = []
|
||||
ds_list = panel_config.get("data source", [])
|
||||
if not isinstance(ds_list, list):
|
||||
print(
|
||||
f"ERROR: {yaml_file} has non-list 'data source' field "
|
||||
f"({type(ds_list).__name__})"
|
||||
)
|
||||
return None
|
||||
|
||||
for ds in ds_list:
|
||||
if not isinstance(ds, dict):
|
||||
print(f"WARNING: {yaml_file} has non-dict data source entry: {ds!r}")
|
||||
continue
|
||||
for key, value in ds.items():
|
||||
if isinstance(value, dict) and "id" in value and "title" in value:
|
||||
norm_id = normalize_table_id(value["id"])
|
||||
data_sources.append({
|
||||
"type": key,
|
||||
"id": value["id"] % 100,
|
||||
"id": norm_id,
|
||||
"title": value["title"],
|
||||
})
|
||||
|
||||
@@ -63,15 +152,65 @@ def parse_panel_config(yaml_file: Path) -> Optional[dict]:
|
||||
}
|
||||
|
||||
|
||||
def build_template_from_directory(
|
||||
directory: Path,
|
||||
existing_panels_by_id: Optional[dict[int, dict]],
|
||||
) -> list[dict]:
|
||||
panels: list[dict] = []
|
||||
errors = 0
|
||||
|
||||
for yaml_file in sorted(directory.glob("*.yaml")):
|
||||
info = parse_panel_config(yaml_file)
|
||||
if info is None:
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
panel_id = info.get("panel_id")
|
||||
|
||||
if (
|
||||
existing_panels_by_id
|
||||
and panel_id is not None
|
||||
and panel_id in existing_panels_by_id
|
||||
):
|
||||
old_panel = existing_panels_by_id[panel_id]
|
||||
|
||||
# Preserve panel_alias unless explicitly set by panel YAML
|
||||
if info.get("panel_alias") is None and "panel_alias" in old_panel:
|
||||
info["panel_alias"] = old_panel["panel_alias"]
|
||||
|
||||
panels.append(info)
|
||||
|
||||
# Deterministic ordering for stable templates
|
||||
panels.sort(key=lambda p: (p["panel_id"], p["file"]))
|
||||
|
||||
if errors:
|
||||
print(
|
||||
f"\nEncountered {errors} panel file(s) with structural errors "
|
||||
"while building template."
|
||||
)
|
||||
|
||||
return panels
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse panel configuration from YAML files"
|
||||
description=(
|
||||
"Parse panel YAML files for an architecture and optionally generate "
|
||||
"a config_template-style YAML describing panel IDs and data sources."
|
||||
)
|
||||
)
|
||||
parser.add_argument("directory", help="Directory containing panel YAML files")
|
||||
parser.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
help="Output YAML file (optional). If omitted, only a summary is printed.",
|
||||
)
|
||||
parser.add_argument("directory", help="Directory containing YAML files")
|
||||
parser.add_argument("output", nargs="?", help="Output YAML file (optional)")
|
||||
parser.add_argument(
|
||||
"--latest-arch",
|
||||
help="Specify this architecture as latest (adds metadata to output)",
|
||||
help=(
|
||||
"Specify this architecture as latest (adds 'latest_arch' metadata "
|
||||
"to the generated template). Only used when an output file is given."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -80,33 +219,45 @@ def main() -> None:
|
||||
print(f"Error: '{args.directory}' is not a valid directory")
|
||||
sys.exit(1)
|
||||
|
||||
results = []
|
||||
for yaml_file in sorted(directory.glob("*.yaml")):
|
||||
parsed = parse_panel_config(yaml_file)
|
||||
if parsed:
|
||||
results.append(parsed)
|
||||
existing_template = None
|
||||
if args.output and Path(args.output).exists():
|
||||
existing_template = cm_utils.load_yaml(Path(args.output))
|
||||
|
||||
if not results:
|
||||
print("No valid panel configurations found.")
|
||||
existing_panels_by_id = {}
|
||||
if existing_template:
|
||||
for p in existing_template.get("panels", []):
|
||||
pid = p.get("panel_id")
|
||||
if pid is not None:
|
||||
existing_panels_by_id[pid] = p
|
||||
|
||||
panels = build_template_from_directory(
|
||||
directory,
|
||||
existing_panels_by_id=existing_panels_by_id if args.output else None,
|
||||
)
|
||||
|
||||
if not panels:
|
||||
print("No valid panel YAML files found; nothing to do.")
|
||||
sys.exit(1)
|
||||
|
||||
for panel in results:
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f"File: {panel['file']}")
|
||||
# Always show a human-readable summary.
|
||||
print(f"Found {len(panels)} panel(s) in {directory}:")
|
||||
for panel in panels:
|
||||
print(f"\nFile: {panel['file']}")
|
||||
print(f"Panel ID: {panel['panel_id']}")
|
||||
print(f"Panel Title: {panel['panel_title']}")
|
||||
if panel.get("panel_alias"):
|
||||
if panel["panel_alias"]:
|
||||
print(f"Panel Alias: {panel['panel_alias']}")
|
||||
print(f"\nData Sources ({len(panel['data_sources'])}):")
|
||||
for ds in panel["data_sources"]:
|
||||
print(f" - {ds['type']}: {ds['id']} - {ds['title']}")
|
||||
|
||||
# Optionally write a template YAML.
|
||||
if args.output:
|
||||
output_data: Any = results
|
||||
output_data: Any = {"panels": panels}
|
||||
if args.latest_arch:
|
||||
output_data = {"latest_arch": args.latest_arch, "panels": results}
|
||||
cm_utils.save_yaml(output_data, args.output, AUTOGEN_TEXT)
|
||||
print(f"\nResults saved to: {args.output}")
|
||||
output_data = {"latest_arch": args.latest_arch, "panels": panels}
|
||||
cm_utils.save_yaml(output_data, Path(args.output))
|
||||
print(f"\nTemplate saved to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
##############################################################################
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
##############################################################################
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def str_representer(dumper, data):
|
||||
if "\n" in data:
|
||||
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=">")
|
||||
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
||||
|
||||
|
||||
yaml.add_representer(str, str_representer)
|
||||
|
||||
|
||||
def load_yaml(filepath: Union[str, Path]) -> dict:
|
||||
with open(filepath) as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
|
||||
def save_yaml(
|
||||
data: dict, filepath: Union[str, Path], header: Optional[str] = None
|
||||
) -> None:
|
||||
with open(filepath, "w") as f:
|
||||
if header:
|
||||
f.write(header)
|
||||
yaml.dump(data, f, sort_keys=False)
|
||||
@@ -0,0 +1,92 @@
|
||||
##############################################################################
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
##############################################################################
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
|
||||
# --- Round-trip YAML (for writing) ---
|
||||
RT_YAML = YAML(typ="rt")
|
||||
RT_YAML.preserve_quotes = True
|
||||
RT_YAML.width = 4096 # prevent unwanted line wrapping
|
||||
RT_YAML.indent(mapping=2, sequence=2, offset=0)
|
||||
RT_YAML.explicit_start = False
|
||||
RT_YAML.explicit_end = False
|
||||
|
||||
# --- Read-only YAML (safe loader) ---
|
||||
RO_YAML = YAML(typ="safe")
|
||||
RO_YAML.width = 4096
|
||||
|
||||
|
||||
def load_yaml(
|
||||
filepath: Union[str, Path],
|
||||
*,
|
||||
round_trip: bool = False,
|
||||
) -> Any:
|
||||
path = Path(filepath)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"YAML file not found: {path}")
|
||||
|
||||
yaml = RT_YAML if round_trip else RO_YAML
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return yaml.load(f) or CommentedMap()
|
||||
|
||||
|
||||
def save_yaml(data: Any, filepath: Union[str, Path]) -> None:
|
||||
path = Path(filepath)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
RT_YAML.dump(data, f)
|
||||
|
||||
|
||||
def strip_existing_header(yaml_data) -> None:
|
||||
ca = getattr(yaml_data, "ca", None)
|
||||
if not ca or not hasattr(ca, "comment") or ca.comment is None:
|
||||
return
|
||||
|
||||
original = ca.comment
|
||||
|
||||
cleaned = []
|
||||
|
||||
for block in original:
|
||||
if block is None:
|
||||
cleaned.append(None)
|
||||
continue
|
||||
|
||||
new_block = [token for token in block if "AUTOGENERATED" not in token.value]
|
||||
|
||||
if not new_block:
|
||||
cleaned.append(None)
|
||||
else:
|
||||
cleaned.append(new_block)
|
||||
|
||||
if len(cleaned) < 2:
|
||||
cleaned.extend([None] * (2 - len(cleaned)))
|
||||
|
||||
ca.comment = cleaned
|
||||
+299
-141
@@ -23,155 +23,318 @@
|
||||
# THE SOFTWARE.
|
||||
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Validate panel YAML files against base template ordering.
|
||||
Checks that panel configs match expected structure, IDs, titles, and data source order.
|
||||
verify_against_config_template.py
|
||||
|
||||
Validate per-architecture panel YAMLs against a shared config template.
|
||||
- Validate structure + ordering only.
|
||||
- Treat any deviation as an error.
|
||||
- Collect all errors and report at end.
|
||||
|
||||
Template format (generated by parse_config_template.py):
|
||||
latest_arch: gfx### (optional)
|
||||
panels:
|
||||
- file: <filename without numeric prefix>
|
||||
panel_id: <normalized panel id>
|
||||
panel_title: <title>
|
||||
panel_alias: <optional>
|
||||
data_sources:
|
||||
- type: metric_table|raw_csv_table|...
|
||||
id: <normalized table id>
|
||||
title: <title>
|
||||
|
||||
Usage:
|
||||
python verify_against_config_template.py <analysis_configs_dir> <template_yaml>
|
||||
python verify_against_config_template.py <analysis_configs_dir> <template_yaml>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
import yaml
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from config_management import utils_ruamel as cm_utils # noqa: E402
|
||||
|
||||
REQUIRED_PANEL_KEYS = ("id", "title", "data source", "metrics_description")
|
||||
OPTIONAL_PANEL_KEYS = ("alias",)
|
||||
DEFAULT_ALLOWED_PANEL_KEYS = set(REQUIRED_PANEL_KEYS) | set(OPTIONAL_PANEL_KEYS)
|
||||
|
||||
|
||||
def normalize_panel_id(panel_id: int) -> int:
|
||||
"""Normalize panel ID by dividing by 100."""
|
||||
return panel_id // 100 if panel_id and panel_id >= 100 else panel_id
|
||||
return panel_id // 100 if panel_id >= 100 else panel_id
|
||||
|
||||
|
||||
def normalize_table_id(table_id: int) -> Optional[int]:
|
||||
"""Normalize table ID using modulo 100."""
|
||||
return table_id % 100 if table_id else None
|
||||
def normalize_table_id(table_id: int) -> int:
|
||||
return table_id % 100
|
||||
|
||||
|
||||
def load_template(template_file: Path) -> dict[int, dict]:
|
||||
"""Load template and create lookup by normalized panel ID."""
|
||||
with open(template_file) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
panels = data.get("panels", [])
|
||||
lookup: dict[int, dict] = {}
|
||||
for panel in panels:
|
||||
pid = normalize_panel_id(panel["panel_id"])
|
||||
lookup[pid] = {
|
||||
"panel_title": panel["panel_title"],
|
||||
"panel_alias": panel.get("panel_alias"),
|
||||
"data_sources": [
|
||||
{"type": ds["type"], "id": ds["id"], "title": ds["title"]}
|
||||
for ds in panel.get("data_sources", [])
|
||||
],
|
||||
}
|
||||
return lookup
|
||||
@dataclass(frozen=True)
|
||||
class TemplateDataSource:
|
||||
type: str
|
||||
id: int
|
||||
title: str
|
||||
|
||||
|
||||
def extract_panel_info(yaml_file: Path) -> Optional[dict]:
|
||||
"""Extract panel config info from YAML file."""
|
||||
with open(yaml_file) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if "Panel Config" not in data:
|
||||
return None
|
||||
|
||||
panel_config = data["Panel Config"]
|
||||
data_sources = []
|
||||
for ds in panel_config.get("data source", []):
|
||||
for key, value in ds.items():
|
||||
if isinstance(value, dict) and "id" in value and "title" in value:
|
||||
data_sources.append({
|
||||
"type": key,
|
||||
"id": normalize_table_id(value["id"]),
|
||||
"title": value["title"],
|
||||
})
|
||||
|
||||
return {
|
||||
"panel_id": normalize_panel_id(panel_config.get("id")),
|
||||
"panel_title": panel_config.get("title"),
|
||||
"data_sources": data_sources,
|
||||
}
|
||||
@dataclass(frozen=True)
|
||||
class TemplatePanel:
|
||||
file: str
|
||||
panel_id: int
|
||||
panel_title: str
|
||||
panel_alias: Any
|
||||
data_sources: tuple[TemplateDataSource, ...]
|
||||
|
||||
|
||||
def validate_panel(
|
||||
yaml_file: Path, panel_info: dict, template: dict[int, dict], stats: dict
|
||||
) -> None:
|
||||
"""Validate a single panel YAML against template."""
|
||||
panel_id = panel_info["panel_id"]
|
||||
file_path = f"{yaml_file.parent.name}/{yaml_file.name}"
|
||||
def _as_str(v: Any) -> str:
|
||||
return "" if v is None else str(v)
|
||||
|
||||
if panel_id not in template:
|
||||
print(f"WARNING [{file_path}]: Panel ID {panel_id} not found in template")
|
||||
stats["warnings"] += 1
|
||||
return
|
||||
|
||||
expected = template[panel_id]
|
||||
def load_template(
|
||||
template_file: Path,
|
||||
) -> tuple[list[TemplatePanel], dict[int, TemplatePanel]]:
|
||||
data = cm_utils.load_yaml(template_file) or {}
|
||||
panels_raw = data.get("panels", [])
|
||||
if not isinstance(panels_raw, list):
|
||||
raise ValueError("Template YAML must contain a top-level 'panels' list")
|
||||
|
||||
panels: list[TemplatePanel] = []
|
||||
by_id: dict[int, TemplatePanel] = {}
|
||||
|
||||
for idx, p in enumerate(panels_raw):
|
||||
if not isinstance(p, dict):
|
||||
raise ValueError(f"Template panels[{idx}] must be a mapping")
|
||||
if "panel_id" not in p or "panel_title" not in p:
|
||||
raise ValueError(
|
||||
f"Template panels[{idx}] missing 'panel_id' or 'panel_title'"
|
||||
)
|
||||
|
||||
pid_raw = p.get("panel_id")
|
||||
if not isinstance(pid_raw, int):
|
||||
raise ValueError(
|
||||
f"Template panels[{idx}].panel_id must be int, got {pid_raw!r}"
|
||||
)
|
||||
pid = normalize_panel_id(pid_raw)
|
||||
|
||||
ds_list = p.get("data_sources", []) or []
|
||||
if not isinstance(ds_list, list):
|
||||
raise ValueError(f"Template panels[{idx}].data_sources must be list")
|
||||
|
||||
ds_out: list[TemplateDataSource] = []
|
||||
for j, ds in enumerate(ds_list):
|
||||
if not isinstance(ds, dict):
|
||||
raise ValueError(
|
||||
f"Template panels[{idx}].data_sources[{j}] must be mapping"
|
||||
)
|
||||
for k in ("type", "id", "title"):
|
||||
if k not in ds:
|
||||
raise ValueError(
|
||||
f"Template panels[{idx}].data_sources[{j}] missing '{k}'"
|
||||
)
|
||||
|
||||
ds_id = ds["id"]
|
||||
if not isinstance(ds_id, int):
|
||||
raise ValueError(
|
||||
f"Template panels[{idx}].data_sources[{j}].id must be int, "
|
||||
f"got {ds_id!r}"
|
||||
)
|
||||
|
||||
ds_out.append(
|
||||
TemplateDataSource(
|
||||
type=_as_str(ds["type"]),
|
||||
id=normalize_table_id(ds_id),
|
||||
title=_as_str(ds["title"]),
|
||||
)
|
||||
)
|
||||
|
||||
panel = TemplatePanel(
|
||||
file=_as_str(p.get("file", "")),
|
||||
panel_id=pid,
|
||||
panel_title=_as_str(p.get("panel_title")),
|
||||
panel_alias=p.get("panel_alias"),
|
||||
data_sources=tuple(ds_out),
|
||||
)
|
||||
|
||||
if pid in by_id:
|
||||
raise ValueError(f"Duplicate panel_id {pid} in template")
|
||||
|
||||
panels.append(panel)
|
||||
by_id[pid] = panel
|
||||
|
||||
return panels, by_id
|
||||
|
||||
|
||||
def extract_panel_info(
|
||||
yaml_file: Path,
|
||||
) -> tuple[Optional[int], dict[str, Any], list[dict[str, Any]]]:
|
||||
"""Return (panel_id, panel_config, extracted_data_sources)."""
|
||||
data = cm_utils.load_yaml(yaml_file) or {}
|
||||
panel_config = data.get("Panel Config")
|
||||
if not isinstance(panel_config, dict):
|
||||
return None, {}, []
|
||||
|
||||
pid_raw = panel_config.get("id")
|
||||
pid = normalize_panel_id(pid_raw) if isinstance(pid_raw, int) else None
|
||||
|
||||
ds_extracted: list[dict[str, Any]] = []
|
||||
ds_list = panel_config.get("data source", [])
|
||||
if isinstance(ds_list, list):
|
||||
for item in ds_list:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
for ds_type, value in item.items():
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and isinstance(value.get("id"), int)
|
||||
and "title" in value
|
||||
):
|
||||
ds_extracted.append({
|
||||
"type": str(ds_type),
|
||||
"id": normalize_table_id(value["id"]),
|
||||
"title": _as_str(value.get("title")),
|
||||
})
|
||||
|
||||
return pid, panel_config, ds_extracted
|
||||
|
||||
|
||||
def validate_arch(
|
||||
arch_dir: Path,
|
||||
template_panels: list[TemplatePanel],
|
||||
template_by_id: dict[int, TemplatePanel],
|
||||
allowed_panel_keys: set[str],
|
||||
) -> list[str]:
|
||||
"""Validate one architecture directory. Returns list of errors."""
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
if panel_info["panel_title"] != expected["panel_title"]:
|
||||
errors.append(
|
||||
f"Panel title mismatch: expected '{expected['panel_title']}', "
|
||||
f"got '{panel_info['panel_title']}'"
|
||||
)
|
||||
panel_files = sorted(arch_dir.glob("*.yaml"))
|
||||
actual_by_id: dict[int, Path] = {}
|
||||
actual_order: list[int] = []
|
||||
|
||||
if len(panel_info["data_sources"]) != len(expected["data_sources"]):
|
||||
errors.append(
|
||||
f"Data source count mismatch: expected {len(expected['data_sources'])}, "
|
||||
f"got {len(panel_info['data_sources'])}"
|
||||
)
|
||||
for f in panel_files:
|
||||
pid, panel_config, ds_actual = extract_panel_info(f)
|
||||
rel = f"{arch_dir.name}/{f.name}"
|
||||
|
||||
for i, actual_ds in enumerate(panel_info["data_sources"]):
|
||||
matching_idx = next(
|
||||
(
|
||||
j
|
||||
for j, exp_ds in enumerate(expected["data_sources"])
|
||||
if actual_ds["id"] == exp_ds["id"]
|
||||
and actual_ds["title"] == exp_ds["title"]
|
||||
and actual_ds["type"] == exp_ds["type"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matching_idx is None:
|
||||
if pid is None:
|
||||
errors.append(f"ERROR [{rel}]: Missing or non-integer Panel Config.id")
|
||||
continue
|
||||
|
||||
# required keys
|
||||
missing = [k for k in REQUIRED_PANEL_KEYS if k not in panel_config]
|
||||
if missing:
|
||||
errors.append(
|
||||
f"Data source {i + 1}: No matching entry in template for "
|
||||
f"{actual_ds['type']} id={actual_ds['id']} title='{actual_ds['title']}'"
|
||||
)
|
||||
elif matching_idx != i:
|
||||
warnings.append(
|
||||
f"Data source {i + 1}: Order mismatch - appears at position {i + 1} "
|
||||
f"but expected at position {matching_idx + 1}"
|
||||
f"ERROR [{rel}]: Missing required Panel Config keys: "
|
||||
f"{', '.join(missing)}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
print(f"ERROR [{file_path}]:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
stats["errors"] += len(errors)
|
||||
stats["failed_files"] += 1
|
||||
elif warnings:
|
||||
print(f"WARNING [{file_path}]:")
|
||||
for warning in warnings:
|
||||
print(f" - {warning}")
|
||||
stats["warnings"] += len(warnings)
|
||||
stats["passed_files"] += 1
|
||||
else:
|
||||
print(f"PASS [{file_path}]")
|
||||
stats["passed_files"] += 1
|
||||
# prohibited keys (unknown keys)
|
||||
for k in panel_config.keys():
|
||||
if k not in allowed_panel_keys:
|
||||
errors.append(
|
||||
f"ERROR [{rel}]: Prohibited/unknown Panel Config key '{k}' "
|
||||
f"(allowed: {sorted(allowed_panel_keys)})"
|
||||
)
|
||||
|
||||
# panel must exist in template
|
||||
if pid not in template_by_id:
|
||||
errors.append(f"ERROR [{rel}]: Panel ID {pid} not found in template")
|
||||
else:
|
||||
expected = template_by_id[pid]
|
||||
actual_title = _as_str(panel_config.get("title"))
|
||||
if actual_title != expected.panel_title:
|
||||
errors.append(
|
||||
f"ERROR [{rel}]: Panel title mismatch for id {pid}: "
|
||||
f"expected '{expected.panel_title}', got '{actual_title}'"
|
||||
)
|
||||
|
||||
# data sources must match count + order strictly
|
||||
if len(ds_actual) != len(expected.data_sources):
|
||||
errors.append(
|
||||
f"ERROR [{rel}]: Data source count mismatch for panel "
|
||||
f"{pid}: expected {len(expected.data_sources)}, "
|
||||
f"got {len(ds_actual)}"
|
||||
)
|
||||
|
||||
for i, exp_ds in enumerate(expected.data_sources):
|
||||
if i >= len(ds_actual):
|
||||
break
|
||||
act = ds_actual[i]
|
||||
if (
|
||||
act["type"] != exp_ds.type
|
||||
or act["id"] != exp_ds.id
|
||||
or act["title"] != exp_ds.title
|
||||
):
|
||||
errors.append(
|
||||
f"ERROR [{rel}]: Data source #{i + 1} mismatch "
|
||||
f"for panel {pid}: expected {exp_ds.type} id={exp_ds.id} "
|
||||
f"title='{exp_ds.title}', got {act['type']} "
|
||||
f"id={act['id']} title='{act['title']}'"
|
||||
)
|
||||
|
||||
# duplicates
|
||||
if pid in actual_by_id:
|
||||
errors.append(
|
||||
f"ERROR [{rel}]: Duplicate panel id {pid} "
|
||||
f"(also in {arch_dir.name}/{actual_by_id[pid].name})"
|
||||
)
|
||||
else:
|
||||
actual_by_id[pid] = f
|
||||
actual_order.append(pid)
|
||||
|
||||
# missing / extra panels
|
||||
expected_ids = [p.panel_id for p in template_panels]
|
||||
actual_ids = set(actual_by_id.keys())
|
||||
expected_set = set(expected_ids)
|
||||
|
||||
for pid in expected_ids:
|
||||
if pid not in actual_ids:
|
||||
errors.append(
|
||||
f"ERROR [{arch_dir.name}]: Missing panel id {pid} required by template"
|
||||
)
|
||||
|
||||
for pid in sorted(actual_ids - expected_set):
|
||||
errors.append(
|
||||
f"ERROR [{arch_dir.name}/{actual_by_id[pid].name}]: "
|
||||
f"Extra panel id {pid} not present in template"
|
||||
)
|
||||
|
||||
# panel ordering (based on file sorting)
|
||||
expected_order = [pid for pid in expected_ids if pid in actual_ids]
|
||||
if actual_order and expected_order and actual_order != expected_order:
|
||||
for i, (a, e) in enumerate(zip(actual_order, expected_order)):
|
||||
if a != e:
|
||||
errors.append(
|
||||
f"ERROR [{arch_dir.name}]: Panel file order mismatch at position "
|
||||
f"{i + 1}: expected panel id {e}, got {a} "
|
||||
"(files must follow template order)"
|
||||
)
|
||||
break
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 3:
|
||||
print(
|
||||
"Usage: python verify_against_config_template.py "
|
||||
"<analysis_configs_dir> <template_yaml>"
|
||||
)
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate per-arch panel YAMLs against a shared config template."
|
||||
)
|
||||
parser.add_argument(
|
||||
"analysis_configs_dir", help="Directory containing architecture subdirs"
|
||||
)
|
||||
parser.add_argument("template_yaml", help="Template YAML (config_template.yaml)")
|
||||
parser.add_argument(
|
||||
"--allow-panel-key",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Allow an additional key under 'Panel Config' (repeatable)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
configs_dir = Path(sys.argv[1])
|
||||
template_file = Path(sys.argv[2])
|
||||
configs_dir = Path(args.analysis_configs_dir)
|
||||
template_file = Path(args.template_yaml)
|
||||
|
||||
if not configs_dir.is_dir():
|
||||
print(f"Error: {configs_dir} is not a directory")
|
||||
@@ -180,45 +343,40 @@ def main() -> None:
|
||||
print(f"Error: {template_file} is not a file")
|
||||
sys.exit(1)
|
||||
|
||||
template_panels, template_by_id = load_template(template_file)
|
||||
allowed_panel_keys = set(DEFAULT_ALLOWED_PANEL_KEYS) | set(args.allow_panel_key)
|
||||
print(f"Loading template from {template_file}")
|
||||
template = load_template(template_file)
|
||||
print(f"Template loaded: {len(template)} panels\n")
|
||||
print(f"Template loaded: {len(template_panels)} panels\n")
|
||||
|
||||
stats = {
|
||||
"total_files": 0,
|
||||
"passed_files": 0,
|
||||
"failed_files": 0,
|
||||
"errors": 0,
|
||||
"warnings": 0,
|
||||
}
|
||||
all_errors: list[str] = []
|
||||
total_arches = 0
|
||||
|
||||
for arch_dir in sorted(configs_dir.iterdir()):
|
||||
if not arch_dir.is_dir():
|
||||
continue
|
||||
total_arches += 1
|
||||
print(f"{'=' * 80}\nValidating architecture: {arch_dir.name}\n{'=' * 80}")
|
||||
for yaml_file in sorted(arch_dir.glob("*.yaml")):
|
||||
stats["total_files"] += 1
|
||||
panel_info = extract_panel_info(yaml_file)
|
||||
if panel_info:
|
||||
validate_panel(yaml_file, panel_info, template, stats)
|
||||
else:
|
||||
print(f"ERROR [{arch_dir.name}/{yaml_file.name}]: Invalid panel config")
|
||||
stats["errors"] += 1
|
||||
stats["failed_files"] += 1
|
||||
arch_errors = validate_arch(
|
||||
arch_dir=arch_dir,
|
||||
template_panels=template_panels,
|
||||
template_by_id=template_by_id,
|
||||
allowed_panel_keys=allowed_panel_keys,
|
||||
)
|
||||
if arch_errors:
|
||||
for e in arch_errors:
|
||||
print(e)
|
||||
all_errors.extend(arch_errors)
|
||||
else:
|
||||
print(f"PASS [{arch_dir.name}]: All panel YAMLs match template")
|
||||
print()
|
||||
|
||||
print(f"{'=' * 80}\nVALIDATION SUMMARY\n{'=' * 80}")
|
||||
print(f"Total files checked: {stats['total_files']}")
|
||||
print(f"Passed: {stats['passed_files']}")
|
||||
print(f"Failed: {stats['failed_files']}")
|
||||
print(f"Total errors: {stats['errors']}")
|
||||
print(f"Total warnings: {stats['warnings']}")
|
||||
print(f"Architectures checked: {total_arches}")
|
||||
print(f"Total errors: {len(all_errors)}")
|
||||
|
||||
if stats["failed_files"] > 0:
|
||||
if all_errors:
|
||||
print("\nValidation FAILED")
|
||||
sys.exit(1)
|
||||
elif stats["warnings"] > 0:
|
||||
print("\nValidation PASSED with warnings")
|
||||
else:
|
||||
print("\nValidation PASSED")
|
||||
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
##############################################################################
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
##############################################################################
|
||||
|
||||
# NOTES
|
||||
#
|
||||
# Read tools/unified_config.yaml and split it into per gfx architecture per panel
|
||||
# config files. WARNING: This script will overwrite existing files under per gfx
|
||||
# architecture folders under src/rocprof_compute_soc/analysis_configs.
|
||||
#
|
||||
# Read tools/unified_config.yaml and split it into metric tables per documentation
|
||||
# section.
|
||||
# WARNING: This script will overwrite existing docs/data/metrics_description.yaml.
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
# Get root directory of the project
|
||||
ROOT_DIR = Path(__file__).parent.parent
|
||||
SOURCE_DIR = ROOT_DIR / "tools"
|
||||
TARGET_DIR = ROOT_DIR / "src" / "rocprof_compute_soc" / "analysis_configs"
|
||||
SETS_TARGET_DIR = ROOT_DIR / "src" / "rocprof_compute_soc" / "profile_configs" / "sets"
|
||||
DOC_TARGET_DIR = ROOT_DIR / "docs" / "data"
|
||||
AUTOGEN_TEXT = (
|
||||
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
|
||||
"Generated from tools/unified_config.yaml. Generated by tools/split_config.py\n"
|
||||
)
|
||||
HASH_FILE = ROOT_DIR / "tools" / "autogen_hash.yaml"
|
||||
HASH_FILE_MAP = {}
|
||||
GFX_VERSIONS = ["gfx908", "gfx90a", "gfx940", "gfx941", "gfx942", "gfx950"]
|
||||
METRIC_ID_TO_NAME_MAP = {gfx_version: {} for gfx_version in GFX_VERSIONS}
|
||||
|
||||
|
||||
def str_representer(dumper, data):
|
||||
if "\n" in data:
|
||||
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
||||
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
||||
|
||||
|
||||
yaml.add_representer(str, str_representer)
|
||||
|
||||
|
||||
def get_autogen_text(config_file="tools/unified_config.yaml"):
|
||||
return (
|
||||
f"# AUTOGENERATED FILE. Only edit for testing purposes, "
|
||||
f"not for development. Generated from {config_file}. "
|
||||
f"Generated by tools/split_config.py\n"
|
||||
)
|
||||
|
||||
|
||||
def update_analysis_config():
|
||||
global METRIC_ID_TO_NAME_MAP
|
||||
|
||||
# Read the unified config file
|
||||
with open(SOURCE_DIR / "unified_config.yaml") as file:
|
||||
unified_config = yaml.safe_load(file)
|
||||
|
||||
# Create per panel config file
|
||||
for panel_config in unified_config["panels"]:
|
||||
new_panel_config = {"Panel Config": {}}
|
||||
new_panel_config["Panel Config"]["id"] = panel_config["id"]
|
||||
new_panel_config["Panel Config"]["title"] = panel_config["title"]
|
||||
|
||||
panel_id_int = panel_config["id"]
|
||||
# Convert int into str with 4 digits
|
||||
panel_id = str(panel_config["id"]).zfill(4)
|
||||
# Replace parentehsis, hyphen, slash and space with underscore
|
||||
# Remove duplicate underscore
|
||||
# Convert to lower case
|
||||
panel_title = re.sub(r"[()\-/ ]+", "_", panel_config["title"])
|
||||
panel_title = "_".join(filter(None, panel_title.split("_")))
|
||||
panel_title = panel_title.lower()
|
||||
|
||||
for gfx_version in GFX_VERSIONS:
|
||||
# Create per gfx architecture folder
|
||||
gfx_dir = TARGET_DIR / gfx_version
|
||||
# Create directory if it doesn't exist
|
||||
if not gfx_dir.exists():
|
||||
gfx_dir.mkdir()
|
||||
print(f"Created directory: {gfx_dir}")
|
||||
|
||||
# Collect metrics for this gfx_version
|
||||
gfx_metrics = []
|
||||
|
||||
# Select metrics from current gfx arch
|
||||
new_panel_config["Panel Config"]["data source"] = []
|
||||
for data_source_index, data_source_config in enumerate(
|
||||
panel_config["data source"]
|
||||
):
|
||||
data_source_config = copy.deepcopy(data_source_config)
|
||||
if "metric_table" in data_source_config:
|
||||
data_source_config["metric_table"]["metric"] = data_source_config[
|
||||
"metric_table"
|
||||
]["metric"][gfx_version]
|
||||
|
||||
# Collect metric names for this gfx version (preserve order)
|
||||
for metric_name in data_source_config["metric_table"][
|
||||
"metric"
|
||||
].keys():
|
||||
if metric_name not in gfx_metrics:
|
||||
gfx_metrics.append(metric_name)
|
||||
|
||||
build_metric_id_mapping(
|
||||
panel_id_int,
|
||||
data_source_index,
|
||||
data_source_config["metric_table"]["metric"],
|
||||
gfx_version,
|
||||
)
|
||||
new_panel_config["Panel Config"]["data source"].append(
|
||||
data_source_config
|
||||
)
|
||||
|
||||
# Only include metric descriptions for metrics that exist in this gfx
|
||||
new_panel_config["Panel Config"]["metrics_description"] = {
|
||||
key: value["plain"].strip()
|
||||
for key, value in panel_config.get("metrics_description", {}).items()
|
||||
if key in gfx_metrics
|
||||
}
|
||||
|
||||
# Write panel config to file
|
||||
filename = TARGET_DIR / gfx_version / f"{panel_id}_{panel_title}.yaml"
|
||||
with open(filename, "w") as file:
|
||||
file.write(get_autogen_text())
|
||||
yaml.dump(new_panel_config, file, sort_keys=False)
|
||||
print(f"File write: {filename}")
|
||||
# Calculate hash of filename
|
||||
HASH_FILE_MAP[str(filename.relative_to(ROOT_DIR))] = hashlib.sha256(
|
||||
filename.read_bytes()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def build_metric_id_mapping(panel_id, data_source_index, metrics, gfx_version):
|
||||
# Build metric id to metric name mapping
|
||||
global METRIC_ID_TO_NAME_MAP
|
||||
for metric_index, metric_name in enumerate(metrics.keys()):
|
||||
metric_id = f"{panel_id // 100}.{data_source_index + 1}.{metric_index}"
|
||||
METRIC_ID_TO_NAME_MAP[gfx_version][str(metric_id)] = metric_name
|
||||
|
||||
|
||||
def update_sets_config():
|
||||
# Create directory if it doesn't exist
|
||||
if not SETS_TARGET_DIR.exists():
|
||||
SETS_TARGET_DIR.mkdir()
|
||||
print(f"Created directory: {SETS_TARGET_DIR}")
|
||||
|
||||
# Read the unified config file
|
||||
with open(SOURCE_DIR / "unified_sets.yaml") as file:
|
||||
unified_sets = yaml.safe_load(file)
|
||||
|
||||
# Create per gfx version file
|
||||
for gfx_version in GFX_VERSIONS:
|
||||
new_sets = {"sets": []}
|
||||
|
||||
for sets in unified_sets["sets"]:
|
||||
# Create new set object for each set
|
||||
current_set = {
|
||||
"title": sets["title"],
|
||||
"set_option": sets["set_option"],
|
||||
"description": sets["description"],
|
||||
"metric": [],
|
||||
}
|
||||
|
||||
for metric_id in sets["metric"][gfx_version]:
|
||||
current_set["metric"].append({
|
||||
metric_id: METRIC_ID_TO_NAME_MAP[gfx_version][str(metric_id)]
|
||||
})
|
||||
|
||||
new_sets["sets"].append(current_set)
|
||||
|
||||
# Write gfx version sets to file
|
||||
filename = SETS_TARGET_DIR / f"{gfx_version}_sets.yaml"
|
||||
with open(filename, "w") as file:
|
||||
file.write(get_autogen_text("tools/unified_sets.yaml"))
|
||||
yaml.dump(new_sets, file, sort_keys=False)
|
||||
print(f"File write: {filename}")
|
||||
# Calculate hash of filename
|
||||
HASH_FILE_MAP[str(filename.relative_to(ROOT_DIR))] = hashlib.sha256(
|
||||
filename.read_bytes()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def update_documentation():
|
||||
# Documentation sections
|
||||
section_panel_map = {
|
||||
"Wavefront launch stats": 701,
|
||||
"Wavefront runtime stats": 702,
|
||||
"Overall instruction mix": 1001,
|
||||
"VALU arithmetic instruction mix": 1002,
|
||||
"MFMA instruction mix": 1004,
|
||||
"Compute Speed-of-Light": 1101,
|
||||
"Pipeline statistics": 1102,
|
||||
"Arithmetic operations": 1103,
|
||||
"LDS Speed-of-Light": 1201,
|
||||
"LDS Statistics": 1202,
|
||||
"vL1D Speed-of-Light": 1601,
|
||||
"Busy / stall metrics": 1501,
|
||||
"Instruction counts": 1502,
|
||||
"Spill / stack metrics": 1503,
|
||||
"L1 Unified Translation Cache (UTCL1)": 1605,
|
||||
"vL1D cache stall metrics": 1602,
|
||||
"vL1D cache access metrics": 1603,
|
||||
"Vector L1 data-return path or Texture Data (TD)": 1504,
|
||||
"L2 Speed-of-Light": 1701,
|
||||
"L2 cache accesses": 1703,
|
||||
"L2-Fabric interface metrics": 1702,
|
||||
"L2 - Fabric interface detailed metrics": 1706,
|
||||
"L2 - Fabric Interface stalls": 1705,
|
||||
"Scalar L1D Speed-of-Light": 1401,
|
||||
"Scalar L1D cache accesses": 1402,
|
||||
"Scalar L1D Cache - L2 Interface": 1403,
|
||||
"L1I Speed-of-Light": 1301,
|
||||
"L1I cache accesses": 1302,
|
||||
"L1I <-> L2 interface": 1303,
|
||||
"Workgroup manager utilizations": 601,
|
||||
"Workgroup Manager - Resource Allocation": 602,
|
||||
"Command processor fetcher (CPF)": 501,
|
||||
"Command processor packet processor (CPC)": 502,
|
||||
"System Speed-of-Light": 201,
|
||||
}
|
||||
|
||||
# Read the unified config file
|
||||
with open(SOURCE_DIR / "unified_config.yaml") as file:
|
||||
unified_config = yaml.safe_load(file)
|
||||
|
||||
panel_metric_map = {}
|
||||
for panel_config in unified_config["panels"]:
|
||||
for data_source in panel_config["data source"]:
|
||||
if "metric_table" in data_source:
|
||||
metrics_info = {}
|
||||
# Metric names from data source
|
||||
metric_names = {
|
||||
metric
|
||||
for _, gfx_data in data_source["metric_table"]["metric"].items()
|
||||
for metric in gfx_data
|
||||
}
|
||||
# Select metrics with descriptions available
|
||||
metric_names = metric_names.intersection(
|
||||
panel_config["metrics_description"].keys()
|
||||
)
|
||||
# Add metrics info
|
||||
for metric_name in sorted(list(metric_names)):
|
||||
metrics_info[metric_name] = {
|
||||
"rst": panel_config["metrics_description"][metric_name][
|
||||
"rst"
|
||||
].strip(),
|
||||
"unit": panel_config["metrics_description"][metric_name][
|
||||
"unit"
|
||||
],
|
||||
}
|
||||
panel_metric_map[data_source["metric_table"]["id"]] = metrics_info
|
||||
|
||||
# Merge panel_metric_map with section_panel_map
|
||||
section_metric_map = {}
|
||||
for section, panel_id in section_panel_map.items():
|
||||
if panel_id in panel_metric_map:
|
||||
section_metric_map[section] = panel_metric_map[panel_id]
|
||||
|
||||
# Write documentation metrics description file
|
||||
filename = DOC_TARGET_DIR / "metrics_description.yaml"
|
||||
with open(filename, "w") as file:
|
||||
file.write(get_autogen_text())
|
||||
yaml.dump(section_metric_map, file, sort_keys=False)
|
||||
print(f"File write: {filename}")
|
||||
# Calculate hash of filename
|
||||
HASH_FILE_MAP[str(filename.relative_to(ROOT_DIR))] = hashlib.sha256(
|
||||
filename.read_bytes()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def update_hash():
|
||||
# Write hash file
|
||||
with open(HASH_FILE, "w") as file:
|
||||
file.write(get_autogen_text())
|
||||
yaml.dump(HASH_FILE_MAP, file, sort_keys=False)
|
||||
print(f"File write: {HASH_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_analysis_config()
|
||||
update_sets_config()
|
||||
update_documentation()
|
||||
update_hash()
|
||||
File diff ditekan karena terlalu besar
Load Diff
@@ -1,176 +0,0 @@
|
||||
---
|
||||
# Pre-defined sets containing a collection of relevant metrics that can be collected in a single pass.
|
||||
# To profile customized set(s), append to this yaml file.
|
||||
|
||||
sets:
|
||||
- title: Compute Throughput Utilization
|
||||
set_option: compute_thruput_util
|
||||
description: Placeholder
|
||||
metric:
|
||||
gfx908:
|
||||
- 11.2.2
|
||||
- 11.2.3
|
||||
gfx90a:
|
||||
- 11.2.3
|
||||
- 11.2.4
|
||||
- 11.2.5
|
||||
- 11.2.6
|
||||
gfx940:
|
||||
- 11.2.2
|
||||
- 11.2.3
|
||||
- 11.2.4
|
||||
- 11.2.5
|
||||
gfx941:
|
||||
- 11.2.2
|
||||
- 11.2.3
|
||||
- 11.2.4
|
||||
- 11.2.5
|
||||
gfx942:
|
||||
- 11.2.2
|
||||
- 11.2.3
|
||||
- 11.2.4
|
||||
- 11.2.5
|
||||
gfx950:
|
||||
- 11.2.2
|
||||
- 11.2.3
|
||||
- 11.2.5
|
||||
- 11.2.6
|
||||
|
||||
- title: Compute Throughput FLOPS
|
||||
set_option: compute_thruput_flops
|
||||
description: Placeholder
|
||||
metric:
|
||||
gfx908:
|
||||
- 2.1.2
|
||||
- 2.1.3
|
||||
- 2.1.4
|
||||
- 2.1.5
|
||||
- 2.1.6
|
||||
gfx90a:
|
||||
- 2.1.2
|
||||
- 2.1.3
|
||||
- 2.1.4
|
||||
- 2.1.5
|
||||
- 2.1.6
|
||||
gfx940:
|
||||
- 2.1.2
|
||||
- 2.1.3
|
||||
- 2.1.4
|
||||
- 2.1.5
|
||||
- 2.1.6
|
||||
- 2.1.7
|
||||
gfx941:
|
||||
- 2.1.2
|
||||
- 2.1.3
|
||||
- 2.1.4
|
||||
- 2.1.5
|
||||
- 2.1.6
|
||||
- 2.1.7
|
||||
gfx942:
|
||||
- 2.1.2
|
||||
- 2.1.3
|
||||
- 2.1.4
|
||||
- 2.1.5
|
||||
- 2.1.6
|
||||
- 2.1.7
|
||||
gfx950:
|
||||
- 2.1.2
|
||||
- 2.1.3
|
||||
- 2.1.4
|
||||
- 2.1.5
|
||||
- 2.1.6
|
||||
- 2.1.8
|
||||
|
||||
- title: Memory Throughput
|
||||
set_option: mem_thruput
|
||||
description: Placeholder
|
||||
metric:
|
||||
gfx908:
|
||||
- 2.1.16
|
||||
- 2.1.17
|
||||
- 16.1.2
|
||||
- 17.1.0
|
||||
gfx90a:
|
||||
- 2.1.16
|
||||
- 2.1.17
|
||||
- 16.1.2
|
||||
- 17.1.0
|
||||
gfx940:
|
||||
- 2.1.17
|
||||
- 2.1.18
|
||||
- 16.1.2
|
||||
- 17.1.0
|
||||
gfx941:
|
||||
- 2.1.17
|
||||
- 2.1.18
|
||||
- 16.1.2
|
||||
- 17.1.0
|
||||
gfx942:
|
||||
- 2.1.17
|
||||
- 2.1.18
|
||||
- 16.1.2
|
||||
- 17.1.0
|
||||
gfx950:
|
||||
- 2.1.18
|
||||
- 2.1.19
|
||||
- 16.1.2
|
||||
- 17.1.0
|
||||
|
||||
- title: Launch Stats
|
||||
set_option: launch_stats
|
||||
description: Placeholder
|
||||
metric:
|
||||
gfx908:
|
||||
- 7.1.0
|
||||
- 7.1.1
|
||||
- 7.1.2
|
||||
- 7.1.5
|
||||
- 7.1.6
|
||||
- 7.1.7
|
||||
- 7.1.8
|
||||
- 7.1.9
|
||||
gfx90a:
|
||||
- 7.1.0
|
||||
- 7.1.1
|
||||
- 7.1.2
|
||||
- 7.1.5
|
||||
- 7.1.6
|
||||
- 7.1.7
|
||||
- 7.1.8
|
||||
- 7.1.9
|
||||
gfx940:
|
||||
- 7.1.0
|
||||
- 7.1.1
|
||||
- 7.1.2
|
||||
- 7.1.5
|
||||
- 7.1.6
|
||||
- 7.1.7
|
||||
- 7.1.8
|
||||
- 7.1.9
|
||||
gfx941:
|
||||
- 7.1.0
|
||||
- 7.1.1
|
||||
- 7.1.2
|
||||
- 7.1.5
|
||||
- 7.1.6
|
||||
- 7.1.7
|
||||
- 7.1.8
|
||||
- 7.1.9
|
||||
gfx942:
|
||||
- 7.1.0
|
||||
- 7.1.1
|
||||
- 7.1.2
|
||||
- 7.1.5
|
||||
- 7.1.6
|
||||
- 7.1.7
|
||||
- 7.1.8
|
||||
- 7.1.9
|
||||
gfx950:
|
||||
- 7.1.0
|
||||
- 7.1.1
|
||||
- 7.1.2
|
||||
- 7.1.5
|
||||
- 7.1.6
|
||||
- 7.1.7
|
||||
- 7.1.8
|
||||
- 7.1.9
|
||||
Reference in New Issue
Block a user