feat: AST-level Python analysis + MI300X benchmark suite
Browse files- AST Transformer: Uses Python's ast module for tree-level analysis
- Detects device assignments, CUDA call sites, embedded kernels
- Identifies env mutations and inline kernel strings structurally
- Not regex — real compiler-level understanding of code structure
- MI300X Benchmark Suite: Ready to run on AMD Developer Cloud
- Device info, memory bandwidth, and GEMM TFLOPS tests
- Saves JSON results as proof of AMD GPU usage
- Wired AST findings into analyzer, API, and orchestrator
- agents/analyzer.py +23 -0
- agents/ast_transformer.py +237 -0
- api.py +1 -0
- benchmark/rocm_benchmark.py +246 -0
agents/analyzer.py
CHANGED
|
@@ -19,6 +19,7 @@ from knowledge.cuda_mappings import (
|
|
| 19 |
HARDWARE_AWARE_MAPPINGS,
|
| 20 |
IMPLICIT_CUDA_PATTERNS,
|
| 21 |
)
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
@dataclass
|
|
@@ -54,6 +55,7 @@ class AnalysisResult:
|
|
| 54 |
migration_level: str = "Easy"
|
| 55 |
code_type: str = "python" # python, cpp, dockerfile, requirements
|
| 56 |
summary: Dict = field(default_factory=dict)
|
|
|
|
| 57 |
trace_log: List[str] = field(default_factory=list)
|
| 58 |
|
| 59 |
|
|
@@ -114,6 +116,10 @@ class AnalyzerAgent:
|
|
| 114 |
self._scan_implicit_assumptions(lines, code, result)
|
| 115 |
result.trace_log.append(f"🧪 Exploration Scan → Found {len(result.implicit_assumptions)} implicit CUDA assumptions")
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
# Build saliency map (per-line risk scoring)
|
| 118 |
self._build_saliency_map(lines, result)
|
| 119 |
result.trace_log.append(f"🎯 Saliency Map → {sum(1 for v in result.saliency_map.values() if v == 'critical')} critical lines identified")
|
|
@@ -500,6 +506,7 @@ class AnalyzerAgent:
|
|
| 500 |
"known_issues": len(result.known_issues),
|
| 501 |
"hardware_issues": len(result.hardware_issues),
|
| 502 |
"implicit_assumptions": len(result.implicit_assumptions),
|
|
|
|
| 503 |
"critical_lines": sum(1 for v in result.saliency_map.values() if v == "critical"),
|
| 504 |
"migration_score": result.migration_score,
|
| 505 |
"migration_health": result.migration_health,
|
|
@@ -509,3 +516,19 @@ class AnalyzerAgent:
|
|
| 509 |
"errors": sum(1 for p in result.detected_patterns if p.severity == "error"),
|
| 510 |
"compatible": sum(1 for p in result.detected_patterns if p.severity == "info"),
|
| 511 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
HARDWARE_AWARE_MAPPINGS,
|
| 20 |
IMPLICIT_CUDA_PATTERNS,
|
| 21 |
)
|
| 22 |
+
from agents.ast_transformer import ASTTransformer
|
| 23 |
|
| 24 |
|
| 25 |
@dataclass
|
|
|
|
| 55 |
migration_level: str = "Easy"
|
| 56 |
code_type: str = "python" # python, cpp, dockerfile, requirements
|
| 57 |
summary: Dict = field(default_factory=dict)
|
| 58 |
+
ast_findings: List[Dict] = field(default_factory=list) # AST-level analysis results
|
| 59 |
trace_log: List[str] = field(default_factory=list)
|
| 60 |
|
| 61 |
|
|
|
|
| 116 |
self._scan_implicit_assumptions(lines, code, result)
|
| 117 |
result.trace_log.append(f"🧪 Exploration Scan → Found {len(result.implicit_assumptions)} implicit CUDA assumptions")
|
| 118 |
|
| 119 |
+
# AST-level analysis (Python only — real compiler-level understanding)
|
| 120 |
+
if result.code_type == "python":
|
| 121 |
+
self._run_ast_analysis(code, result)
|
| 122 |
+
|
| 123 |
# Build saliency map (per-line risk scoring)
|
| 124 |
self._build_saliency_map(lines, result)
|
| 125 |
result.trace_log.append(f"🎯 Saliency Map → {sum(1 for v in result.saliency_map.values() if v == 'critical')} critical lines identified")
|
|
|
|
| 506 |
"known_issues": len(result.known_issues),
|
| 507 |
"hardware_issues": len(result.hardware_issues),
|
| 508 |
"implicit_assumptions": len(result.implicit_assumptions),
|
| 509 |
+
"ast_findings": len(result.ast_findings),
|
| 510 |
"critical_lines": sum(1 for v in result.saliency_map.values() if v == "critical"),
|
| 511 |
"migration_score": result.migration_score,
|
| 512 |
"migration_health": result.migration_health,
|
|
|
|
| 516 |
"errors": sum(1 for p in result.detected_patterns if p.severity == "error"),
|
| 517 |
"compatible": sum(1 for p in result.detected_patterns if p.severity == "info"),
|
| 518 |
}
|
| 519 |
+
|
| 520 |
+
def _run_ast_analysis(self, code: str, result: AnalysisResult):
|
| 521 |
+
"""
|
| 522 |
+
Run AST-level analysis on Python code using Python's ast module.
|
| 523 |
+
This is a real compiler-level pass — not regex.
|
| 524 |
+
"""
|
| 525 |
+
transformer = ASTTransformer()
|
| 526 |
+
findings, trace = transformer.analyze(code)
|
| 527 |
+
result.ast_findings = findings
|
| 528 |
+
result.trace_log.extend(trace)
|
| 529 |
+
|
| 530 |
+
# Count critical AST findings
|
| 531 |
+
critical = sum(1 for f in findings if f.get("severity") == "critical")
|
| 532 |
+
if critical > 0:
|
| 533 |
+
result.trace_log.append(f"🌳 AST: {critical} critical finding(s) — embedded kernels require manual review")
|
| 534 |
+
|
agents/ast_transformer.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ROCm Forge — AST-Level Python Transformer
|
| 3 |
+
Uses Python's ast module to perform tree-level code transformation,
|
| 4 |
+
going beyond regex to understand code STRUCTURE.
|
| 5 |
+
|
| 6 |
+
This is the critical differentiator from hipify and other string-based tools.
|
| 7 |
+
"""
|
| 8 |
+
import ast
|
| 9 |
+
import re
|
| 10 |
+
from typing import List, Tuple, Dict
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CUDANodeVisitor(ast.NodeVisitor):
|
| 14 |
+
"""
|
| 15 |
+
AST visitor that walks the Python syntax tree and detects
|
| 16 |
+
CUDA-specific constructs at the structural level.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(self):
|
| 20 |
+
self.findings: List[Dict] = []
|
| 21 |
+
self.device_vars: Dict[str, int] = {} # var_name -> line_no
|
| 22 |
+
self.cuda_call_sites: List[Dict] = []
|
| 23 |
+
self.env_mutations: List[Dict] = []
|
| 24 |
+
self.string_literals_with_cuda: List[Dict] = []
|
| 25 |
+
|
| 26 |
+
def visit_Assign(self, node: ast.Assign):
|
| 27 |
+
"""Detect device = torch.device('cuda:0') assignments."""
|
| 28 |
+
if isinstance(node.value, ast.Call):
|
| 29 |
+
func = node.value
|
| 30 |
+
func_name = self._get_func_name(func)
|
| 31 |
+
|
| 32 |
+
# torch.device("cuda:X") pattern
|
| 33 |
+
if func_name == "torch.device" and func.args:
|
| 34 |
+
arg = func.args[0]
|
| 35 |
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
| 36 |
+
if "cuda" in arg.value:
|
| 37 |
+
for target in node.targets:
|
| 38 |
+
if isinstance(target, ast.Name):
|
| 39 |
+
self.device_vars[target.id] = node.lineno
|
| 40 |
+
self.findings.append({
|
| 41 |
+
"line": node.lineno,
|
| 42 |
+
"type": "device_assignment",
|
| 43 |
+
"original": f'torch.device("{arg.value}")',
|
| 44 |
+
"suggestion": f'torch.device("{arg.value}") # Works on ROCm — PyTorch ROCm uses cuda namespace',
|
| 45 |
+
"severity": "info",
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
# os.environ["CUDA_VISIBLE_DEVICES"] = ... pattern
|
| 49 |
+
if isinstance(node.value, ast.Constant) and len(node.targets) == 1:
|
| 50 |
+
target = node.targets[0]
|
| 51 |
+
if isinstance(target, ast.Subscript):
|
| 52 |
+
if isinstance(target.slice, ast.Constant) and isinstance(target.slice.value, str):
|
| 53 |
+
if "CUDA" in target.slice.value:
|
| 54 |
+
self.env_mutations.append({
|
| 55 |
+
"line": node.lineno,
|
| 56 |
+
"var": target.slice.value,
|
| 57 |
+
"type": "env_mutation",
|
| 58 |
+
})
|
| 59 |
+
|
| 60 |
+
self.generic_visit(node)
|
| 61 |
+
|
| 62 |
+
def visit_Call(self, node: ast.Call):
|
| 63 |
+
"""Detect CUDA API calls: .cuda(), torch.cuda.*, etc."""
|
| 64 |
+
func_name = self._get_func_name(node)
|
| 65 |
+
|
| 66 |
+
# .cuda() method calls
|
| 67 |
+
if func_name and func_name.endswith(".cuda"):
|
| 68 |
+
self.cuda_call_sites.append({
|
| 69 |
+
"line": node.lineno,
|
| 70 |
+
"call": func_name,
|
| 71 |
+
"type": "cuda_method",
|
| 72 |
+
"note": ".cuda() works on ROCm — maps to HIP backend",
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
# torch.cuda.is_available(), torch.cuda.device_count(), etc.
|
| 76 |
+
if func_name and "torch.cuda" in func_name:
|
| 77 |
+
self.cuda_call_sites.append({
|
| 78 |
+
"line": node.lineno,
|
| 79 |
+
"call": func_name,
|
| 80 |
+
"type": "torch_cuda_api",
|
| 81 |
+
"note": "torch.cuda.* APIs work on ROCm via HIP backend",
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
# load_inline / cpp_extension calls
|
| 85 |
+
if func_name and ("load_inline" in func_name or "load" in func_name):
|
| 86 |
+
for kw in node.keywords:
|
| 87 |
+
if kw.arg == "cuda_sources":
|
| 88 |
+
self.findings.append({
|
| 89 |
+
"line": node.lineno,
|
| 90 |
+
"type": "inline_kernel",
|
| 91 |
+
"original": "cuda_sources=[...]",
|
| 92 |
+
"suggestion": "hip_sources=[...] (rename parameter for clarity)",
|
| 93 |
+
"severity": "review",
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
self.generic_visit(node)
|
| 97 |
+
|
| 98 |
+
def visit_Constant(self, node: ast.Constant):
|
| 99 |
+
"""Detect string literals containing CUDA code."""
|
| 100 |
+
if isinstance(node.value, str) and len(node.value) > 50:
|
| 101 |
+
cuda_indicators = [
|
| 102 |
+
"cuda_runtime.h", "cudaMalloc", "cudaFree",
|
| 103 |
+
"__global__", "cudaMemcpy", "cudaDeviceSynchronize",
|
| 104 |
+
"<<<", ">>>",
|
| 105 |
+
]
|
| 106 |
+
if any(ind in node.value for ind in cuda_indicators):
|
| 107 |
+
self.string_literals_with_cuda.append({
|
| 108 |
+
"line": node.lineno,
|
| 109 |
+
"length": len(node.value),
|
| 110 |
+
"type": "embedded_cuda_kernel",
|
| 111 |
+
"note": "Embedded CUDA C/C++ kernel detected in string literal",
|
| 112 |
+
})
|
| 113 |
+
self.generic_visit(node)
|
| 114 |
+
|
| 115 |
+
def visit_If(self, node: ast.If):
|
| 116 |
+
"""Detect `if not torch.cuda.is_available()` guards."""
|
| 117 |
+
test_src = ast.dump(node.test)
|
| 118 |
+
if "torch" in test_src and "cuda" in test_src:
|
| 119 |
+
self.findings.append({
|
| 120 |
+
"line": node.lineno,
|
| 121 |
+
"type": "cuda_guard",
|
| 122 |
+
"original": "CUDA availability check",
|
| 123 |
+
"suggestion": "Works on ROCm — torch.cuda.is_available() returns True with HIP backend",
|
| 124 |
+
"severity": "info",
|
| 125 |
+
})
|
| 126 |
+
self.generic_visit(node)
|
| 127 |
+
|
| 128 |
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
| 129 |
+
"""Detect imports from CUDA-specific modules."""
|
| 130 |
+
if node.module and "cuda" in node.module.lower():
|
| 131 |
+
self.findings.append({
|
| 132 |
+
"line": node.lineno,
|
| 133 |
+
"type": "cuda_import",
|
| 134 |
+
"original": f"from {node.module} import ...",
|
| 135 |
+
"suggestion": "Check ROCm compatibility for this import",
|
| 136 |
+
"severity": "review",
|
| 137 |
+
})
|
| 138 |
+
self.generic_visit(node)
|
| 139 |
+
|
| 140 |
+
def _get_func_name(self, node: ast.Call) -> str:
|
| 141 |
+
"""Extract dotted function name from a Call node."""
|
| 142 |
+
if isinstance(node.func, ast.Name):
|
| 143 |
+
return node.func.id
|
| 144 |
+
elif isinstance(node.func, ast.Attribute):
|
| 145 |
+
parts = []
|
| 146 |
+
current = node.func
|
| 147 |
+
while isinstance(current, ast.Attribute):
|
| 148 |
+
parts.append(current.attr)
|
| 149 |
+
current = current.value
|
| 150 |
+
if isinstance(current, ast.Name):
|
| 151 |
+
parts.append(current.id)
|
| 152 |
+
return ".".join(reversed(parts))
|
| 153 |
+
return ""
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class ASTTransformer:
|
| 157 |
+
"""
|
| 158 |
+
AST-level Python code transformer.
|
| 159 |
+
Performs structural analysis beyond regex — understands scope, assignment,
|
| 160 |
+
function calls, and string literal contexts.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
def __init__(self):
|
| 164 |
+
self.trace_log: List[str] = []
|
| 165 |
+
self.ast_findings: List[Dict] = []
|
| 166 |
+
|
| 167 |
+
def analyze(self, code: str) -> Tuple[List[Dict], List[str]]:
|
| 168 |
+
"""
|
| 169 |
+
Perform AST-level analysis on Python code.
|
| 170 |
+
Returns (findings, trace_log).
|
| 171 |
+
Falls back gracefully if code can't be parsed.
|
| 172 |
+
"""
|
| 173 |
+
self.trace_log = []
|
| 174 |
+
self.ast_findings = []
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
tree = ast.parse(code)
|
| 178 |
+
self.trace_log.append("🌳 AST parse successful — performing tree-level analysis")
|
| 179 |
+
except SyntaxError as e:
|
| 180 |
+
self.trace_log.append(f"🌳 AST parse failed (line {e.lineno}) — code contains non-Python syntax, using regex fallback")
|
| 181 |
+
return [], self.trace_log
|
| 182 |
+
|
| 183 |
+
visitor = CUDANodeVisitor()
|
| 184 |
+
visitor.visit(tree)
|
| 185 |
+
|
| 186 |
+
# Aggregate findings
|
| 187 |
+
self.ast_findings = visitor.findings.copy()
|
| 188 |
+
|
| 189 |
+
# Report device variables
|
| 190 |
+
for var, line in visitor.device_vars.items():
|
| 191 |
+
self.ast_findings.append({
|
| 192 |
+
"line": line,
|
| 193 |
+
"type": "device_variable",
|
| 194 |
+
"original": f'{var} = torch.device("cuda:...")',
|
| 195 |
+
"suggestion": "Device variable tracked — all downstream .to(device) calls are AMD-safe",
|
| 196 |
+
"severity": "info",
|
| 197 |
+
})
|
| 198 |
+
|
| 199 |
+
# Report embedded kernels
|
| 200 |
+
for kernel in visitor.string_literals_with_cuda:
|
| 201 |
+
self.ast_findings.append({
|
| 202 |
+
"line": kernel["line"],
|
| 203 |
+
"type": "embedded_kernel",
|
| 204 |
+
"original": f"Inline CUDA kernel ({kernel['length']} chars)",
|
| 205 |
+
"suggestion": "Embedded kernel requires hipify transformation of string contents",
|
| 206 |
+
"severity": "critical",
|
| 207 |
+
})
|
| 208 |
+
|
| 209 |
+
# Report CUDA call sites
|
| 210 |
+
for call in visitor.cuda_call_sites:
|
| 211 |
+
self.ast_findings.append({
|
| 212 |
+
"line": call["line"],
|
| 213 |
+
"type": call["type"],
|
| 214 |
+
"original": call["call"],
|
| 215 |
+
"suggestion": call["note"],
|
| 216 |
+
"severity": "info",
|
| 217 |
+
})
|
| 218 |
+
|
| 219 |
+
# Report env mutations
|
| 220 |
+
for env in visitor.env_mutations:
|
| 221 |
+
self.ast_findings.append({
|
| 222 |
+
"line": env["line"],
|
| 223 |
+
"type": "env_mutation",
|
| 224 |
+
"original": f'os.environ["{env["var"]}"]',
|
| 225 |
+
"suggestion": f'Needs migration to ROCm equivalent env var',
|
| 226 |
+
"severity": "warning",
|
| 227 |
+
})
|
| 228 |
+
|
| 229 |
+
# Summary
|
| 230 |
+
self.trace_log.append(
|
| 231 |
+
f"🌳 AST Analysis: {len(visitor.device_vars)} device vars, "
|
| 232 |
+
f"{len(visitor.cuda_call_sites)} CUDA calls, "
|
| 233 |
+
f"{len(visitor.string_literals_with_cuda)} embedded kernels, "
|
| 234 |
+
f"{len(visitor.env_mutations)} env mutations"
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
return self.ast_findings, self.trace_log
|
api.py
CHANGED
|
@@ -71,6 +71,7 @@ async def migrate_code(request: MigrationRequest):
|
|
| 71 |
} for p in result.analysis.hardware_issues
|
| 72 |
],
|
| 73 |
"implicit_assumptions": result.analysis.implicit_assumptions,
|
|
|
|
| 74 |
"saliency_map": result.analysis.saliency_map,
|
| 75 |
"detected_patterns": [
|
| 76 |
{
|
|
|
|
| 71 |
} for p in result.analysis.hardware_issues
|
| 72 |
],
|
| 73 |
"implicit_assumptions": result.analysis.implicit_assumptions,
|
| 74 |
+
"ast_findings": result.analysis.ast_findings,
|
| 75 |
"saliency_map": result.analysis.saliency_map,
|
| 76 |
"detected_patterns": [
|
| 77 |
{
|
benchmark/rocm_benchmark.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ROCm Forge — MI300X Benchmark Suite
|
| 3 |
+
Ready to run on AMD Developer Cloud when credits arrive.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python benchmark/rocm_benchmark.py --all
|
| 7 |
+
python benchmark/rocm_benchmark.py --device-info
|
| 8 |
+
python benchmark/rocm_benchmark.py --memory
|
| 9 |
+
python benchmark/rocm_benchmark.py --compute
|
| 10 |
+
"""
|
| 11 |
+
import argparse
|
| 12 |
+
import time
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def check_rocm_available():
|
| 20 |
+
"""Check if ROCm/HIP runtime is available."""
|
| 21 |
+
try:
|
| 22 |
+
import torch
|
| 23 |
+
if not torch.cuda.is_available():
|
| 24 |
+
print("❌ No GPU detected. ROCm/HIP runtime not available.")
|
| 25 |
+
print(" Make sure you're running on an AMD GPU instance with ROCm installed.")
|
| 26 |
+
return False
|
| 27 |
+
device_name = torch.cuda.get_device_name(0)
|
| 28 |
+
print(f"✅ GPU detected: {device_name}")
|
| 29 |
+
print(f" PyTorch version: {torch.__version__}")
|
| 30 |
+
hip_version = getattr(torch.version, 'hip', None)
|
| 31 |
+
cuda_version = getattr(torch.version, 'cuda', None)
|
| 32 |
+
if hip_version:
|
| 33 |
+
print(f" HIP version: {hip_version}")
|
| 34 |
+
elif cuda_version:
|
| 35 |
+
print(f" CUDA version: {cuda_version}")
|
| 36 |
+
return True
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"❌ Error checking GPU: {e}")
|
| 39 |
+
return False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def device_info_benchmark():
|
| 43 |
+
"""Collect detailed device information for the hackathon submission."""
|
| 44 |
+
import torch
|
| 45 |
+
|
| 46 |
+
results = {
|
| 47 |
+
"timestamp": datetime.now().isoformat(),
|
| 48 |
+
"pytorch_version": torch.__version__,
|
| 49 |
+
"hip_version": getattr(torch.version, 'hip', 'N/A'),
|
| 50 |
+
"gpu_count": torch.cuda.device_count(),
|
| 51 |
+
"devices": [],
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
for i in range(torch.cuda.device_count()):
|
| 55 |
+
props = torch.cuda.get_device_properties(i)
|
| 56 |
+
device_info = {
|
| 57 |
+
"index": i,
|
| 58 |
+
"name": props.name,
|
| 59 |
+
"total_memory_gb": round(props.total_mem / (1024**3), 2),
|
| 60 |
+
"multi_processor_count": props.multi_processor_count,
|
| 61 |
+
"major": props.major,
|
| 62 |
+
"minor": props.minor,
|
| 63 |
+
}
|
| 64 |
+
results["devices"].append(device_info)
|
| 65 |
+
|
| 66 |
+
print("\n📊 Device Information")
|
| 67 |
+
print("=" * 50)
|
| 68 |
+
print(json.dumps(results, indent=2))
|
| 69 |
+
return results
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def memory_benchmark():
|
| 73 |
+
"""Test GPU memory allocation and bandwidth."""
|
| 74 |
+
import torch
|
| 75 |
+
|
| 76 |
+
device = torch.device("cuda:0")
|
| 77 |
+
results = {"tests": []}
|
| 78 |
+
|
| 79 |
+
sizes_mb = [256, 512, 1024, 2048, 4096]
|
| 80 |
+
print("\n💾 Memory Benchmark")
|
| 81 |
+
print("=" * 50)
|
| 82 |
+
|
| 83 |
+
for size_mb in sizes_mb:
|
| 84 |
+
n_elements = (size_mb * 1024 * 1024) // 4 # float32
|
| 85 |
+
torch.cuda.synchronize()
|
| 86 |
+
|
| 87 |
+
# Allocation
|
| 88 |
+
start = time.perf_counter()
|
| 89 |
+
tensor = torch.zeros(n_elements, dtype=torch.float32, device=device)
|
| 90 |
+
torch.cuda.synchronize()
|
| 91 |
+
alloc_time = (time.perf_counter() - start) * 1000
|
| 92 |
+
|
| 93 |
+
# Fill (bandwidth test)
|
| 94 |
+
start = time.perf_counter()
|
| 95 |
+
tensor.fill_(1.0)
|
| 96 |
+
torch.cuda.synchronize()
|
| 97 |
+
fill_time = (time.perf_counter() - start) * 1000
|
| 98 |
+
bandwidth_gbps = (size_mb / 1024) / (fill_time / 1000) if fill_time > 0 else 0
|
| 99 |
+
|
| 100 |
+
# Copy D2D
|
| 101 |
+
start = time.perf_counter()
|
| 102 |
+
tensor2 = tensor.clone()
|
| 103 |
+
torch.cuda.synchronize()
|
| 104 |
+
copy_time = (time.perf_counter() - start) * 1000
|
| 105 |
+
|
| 106 |
+
result = {
|
| 107 |
+
"size_mb": size_mb,
|
| 108 |
+
"alloc_ms": round(alloc_time, 2),
|
| 109 |
+
"fill_ms": round(fill_time, 2),
|
| 110 |
+
"bandwidth_gbps": round(bandwidth_gbps, 2),
|
| 111 |
+
"copy_d2d_ms": round(copy_time, 2),
|
| 112 |
+
}
|
| 113 |
+
results["tests"].append(result)
|
| 114 |
+
print(f" {size_mb:>5} MB → alloc: {alloc_time:6.2f}ms fill: {fill_time:6.2f}ms bandwidth: {bandwidth_gbps:7.2f} GB/s D2D: {copy_time:6.2f}ms")
|
| 115 |
+
|
| 116 |
+
del tensor, tensor2
|
| 117 |
+
torch.cuda.empty_cache()
|
| 118 |
+
|
| 119 |
+
return results
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def compute_benchmark():
|
| 123 |
+
"""Test raw compute performance with matrix operations."""
|
| 124 |
+
import torch
|
| 125 |
+
|
| 126 |
+
device = torch.device("cuda:0")
|
| 127 |
+
results = {"tests": []}
|
| 128 |
+
|
| 129 |
+
sizes = [1024, 2048, 4096, 8192]
|
| 130 |
+
print("\n⚡ Compute Benchmark (GEMM)")
|
| 131 |
+
print("=" * 50)
|
| 132 |
+
|
| 133 |
+
for n in sizes:
|
| 134 |
+
a = torch.randn(n, n, device=device, dtype=torch.float32)
|
| 135 |
+
b = torch.randn(n, n, device=device, dtype=torch.float32)
|
| 136 |
+
|
| 137 |
+
# Warmup
|
| 138 |
+
for _ in range(3):
|
| 139 |
+
torch.mm(a, b)
|
| 140 |
+
torch.cuda.synchronize()
|
| 141 |
+
|
| 142 |
+
# Benchmark
|
| 143 |
+
num_runs = 10
|
| 144 |
+
start = time.perf_counter()
|
| 145 |
+
for _ in range(num_runs):
|
| 146 |
+
torch.mm(a, b)
|
| 147 |
+
torch.cuda.synchronize()
|
| 148 |
+
elapsed = (time.perf_counter() - start) / num_runs * 1000
|
| 149 |
+
|
| 150 |
+
# TFLOPS = 2 * N^3 / time
|
| 151 |
+
tflops = (2 * n**3) / (elapsed / 1000) / 1e12
|
| 152 |
+
|
| 153 |
+
result = {
|
| 154 |
+
"matrix_size": n,
|
| 155 |
+
"avg_ms": round(elapsed, 2),
|
| 156 |
+
"tflops": round(tflops, 2),
|
| 157 |
+
}
|
| 158 |
+
results["tests"].append(result)
|
| 159 |
+
print(f" {n:>5}x{n} → {elapsed:8.2f} ms ({tflops:6.2f} TFLOPS)")
|
| 160 |
+
|
| 161 |
+
del a, b
|
| 162 |
+
torch.cuda.empty_cache()
|
| 163 |
+
|
| 164 |
+
# FP16 test
|
| 165 |
+
print("\n⚡ Compute Benchmark (GEMM FP16)")
|
| 166 |
+
print("=" * 50)
|
| 167 |
+
for n in [4096, 8192]:
|
| 168 |
+
a = torch.randn(n, n, device=device, dtype=torch.float16)
|
| 169 |
+
b = torch.randn(n, n, device=device, dtype=torch.float16)
|
| 170 |
+
|
| 171 |
+
for _ in range(3):
|
| 172 |
+
torch.mm(a, b)
|
| 173 |
+
torch.cuda.synchronize()
|
| 174 |
+
|
| 175 |
+
num_runs = 10
|
| 176 |
+
start = time.perf_counter()
|
| 177 |
+
for _ in range(num_runs):
|
| 178 |
+
torch.mm(a, b)
|
| 179 |
+
torch.cuda.synchronize()
|
| 180 |
+
elapsed = (time.perf_counter() - start) / num_runs * 1000
|
| 181 |
+
tflops = (2 * n**3) / (elapsed / 1000) / 1e12
|
| 182 |
+
|
| 183 |
+
result = {
|
| 184 |
+
"matrix_size": f"{n}_fp16",
|
| 185 |
+
"avg_ms": round(elapsed, 2),
|
| 186 |
+
"tflops": round(tflops, 2),
|
| 187 |
+
}
|
| 188 |
+
results["tests"].append(result)
|
| 189 |
+
print(f" {n:>5}x{n} FP16 → {elapsed:8.2f} ms ({tflops:6.2f} TFLOPS)")
|
| 190 |
+
|
| 191 |
+
del a, b
|
| 192 |
+
torch.cuda.empty_cache()
|
| 193 |
+
|
| 194 |
+
return results
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def run_all_benchmarks():
|
| 198 |
+
"""Run all benchmarks and save results."""
|
| 199 |
+
print("🔥 ROCm Forge — MI300X Benchmark Suite")
|
| 200 |
+
print("=" * 50)
|
| 201 |
+
|
| 202 |
+
if not check_rocm_available():
|
| 203 |
+
print("\n⚠️ Run this on AMD Developer Cloud with MI300X GPU.")
|
| 204 |
+
print(" python benchmark/rocm_benchmark.py --all")
|
| 205 |
+
return
|
| 206 |
+
|
| 207 |
+
results = {
|
| 208 |
+
"benchmark_version": "1.0",
|
| 209 |
+
"timestamp": datetime.now().isoformat(),
|
| 210 |
+
"device_info": device_info_benchmark(),
|
| 211 |
+
"memory": memory_benchmark(),
|
| 212 |
+
"compute": compute_benchmark(),
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
# Save results
|
| 216 |
+
os.makedirs("benchmark/results", exist_ok=True)
|
| 217 |
+
output_file = f"benchmark/results/benchmark_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 218 |
+
with open(output_file, "w") as f:
|
| 219 |
+
json.dump(results, f, indent=2)
|
| 220 |
+
|
| 221 |
+
print(f"\n✅ Results saved to {output_file}")
|
| 222 |
+
print("📸 Use these results as proof of AMD GPU usage in your hackathon submission!")
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if __name__ == "__main__":
|
| 226 |
+
parser = argparse.ArgumentParser(description="ROCm Forge MI300X Benchmark Suite")
|
| 227 |
+
parser.add_argument("--all", action="store_true", help="Run all benchmarks")
|
| 228 |
+
parser.add_argument("--device-info", action="store_true", help="Show device information")
|
| 229 |
+
parser.add_argument("--memory", action="store_true", help="Run memory benchmarks")
|
| 230 |
+
parser.add_argument("--compute", action="store_true", help="Run compute benchmarks")
|
| 231 |
+
args = parser.parse_args()
|
| 232 |
+
|
| 233 |
+
if args.all:
|
| 234 |
+
run_all_benchmarks()
|
| 235 |
+
elif args.device_info:
|
| 236 |
+
if check_rocm_available():
|
| 237 |
+
device_info_benchmark()
|
| 238 |
+
elif args.memory:
|
| 239 |
+
if check_rocm_available():
|
| 240 |
+
memory_benchmark()
|
| 241 |
+
elif args.compute:
|
| 242 |
+
if check_rocm_available():
|
| 243 |
+
compute_benchmark()
|
| 244 |
+
else:
|
| 245 |
+
parser.print_help()
|
| 246 |
+
print("\n💡 Quick start: python benchmark/rocm_benchmark.py --all")
|