Repository navigation
Add ROCm/AMD GPU support for kernel generation and optimization - #153
dnikolaev-amd wants to merge 26 commits into
Conversation
Adds parallel ROCm/HIP support to KernelAgent alongside the existing NVIDIA CUDA and Intel XPU backends. All changes are modular and do not modify the CUDA or XPU code paths. ## Platform detection & device handling - Add `--target-platform rocm` option to platform registry - ROCm device string is `"cuda"` (PyTorch uses HIP-CUDA compatibility layer) - Platform guidance instructs LLM to use wavefront size 64 (vs NVIDIA warp size 32) - Preferred block sizes are multiples of 64 for AMD CDNA architecture ## Profiling (rocprof replaces NCU for ROCm path) New files: - `kernel_perf_agent/kernel_opt/profiler/rocprof_profiler.py`: Wraps `rocprof` (or `rocprofv3`) to collect hardware PMC counters in two passes: (1) `--stats` for kernel timing, (2) `-i input.txt` for SQ/TCC counters - `triton_kernel_agent/opt_worker_component/profiling/rocprof_wrapper_factory.py`: Factory generating rocprof-compatible benchmark wrapper scripts - `triton_kernel_agent/opt_worker_component/profiling/rocprof_wrapper_template.j2`: Jinja2 template for the wrapper (mirrors ncu_wrapper_template.j2 but uses `torch.cuda` which works on ROCm via HIP) - `triton_kernel_agent/opt_worker_component/profiling/rocm_kernel_profiler.py`: Drop-in replacement for `KernelProfiler` using rocprof Counter mapping: - Compute utilization: SQ_WAVES, SQ_INSTS_VALU, SQ_INSTS_SALU - Memory bandwidth: TCC_EA_RDREQ/WRREQ, FETCH_SIZE, WRITE_SIZE - Cache hit rate: TCC_HIT / (TCC_HIT + TCC_MISS) - Stall analysis: SQ_WAIT_INST_ANY ## Roofline analysis (heuristic SOL from rocprof counters) New file: - `kernel_perf_agent/kernel_opt/roofline/rocm_roofline.py`: `ROCmRooflineAnalyzer` derives heuristic compute/memory SOL estimates from raw PMC counters (VALU utilization and VMEM fraction x L2 miss amplifier). Same interface as `RooflineAnalyzer` (NCU SOL-based). ## Hardware specs for roofline Extended `gpu_specs_database.py` with AMD Instinct GPUs: - AMD Instinct MI300X (CDNA3/gfx942): 1307.4 TFLOPS BF16, 5.3 TB/s HBM3, 304 CUs - AMD Instinct MI300A (CDNA3/gfx942 APU): 980.6 TFLOPS BF16, 3.2 TB/s HBM3 - AMD Instinct MI350X (CDNA4/gfx950): ~2304 TFLOPS BF16, ~8 TB/s HBM3E - AMD Instinct MI250X (CDNA2/gfx90a): 383 TFLOPS BF16, 3.3 TB/s HBM2e ## Platform registry integration New file: - `triton_kernel_agent/platform/rocm.py`: Full set of ROCm platform implementations (ROCmVerifier, ROCmBenchmarker, ROCmWorkerRunner, ROCmAcceleratorSpecsProvider, ROCmKernelProfilerWrapper, ROCmRooflineAnalyzerWrapper, ROCmBottleneckAnalyzer, ROCmRAGPrescriber) Updated files: - `triton_kernel_agent/platform/registry.py`: Register all ROCm components under the "rocm" implementation name - `triton_kernel_agent/platform/__init__.py`: Export ROCm classes Testing on MI300X/MI350X hardware is planned. The noop platform can be used in CI as a stand-in for rocprof-free environments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…profv3 compatibility Fixes three categories of issues found during MI300X E2E testing: 1. Restore accidentally deleted methods from ROCm port (0300c77): - _profile_and_analyze(): profiles kernel with rocprof, analyzes bottlenecks - _generate_optimized_kernel(): calls LLM to generate optimized kernel code - _profile_kernel_for_sol(): profiles for Speed of Light metrics These methods were deleted while their call sites remained, causing AttributeError crashes during optimization rounds. 2. Add synthetic bottleneck analysis fallback: - When rocprof profiling fails (GPU contention, version mismatch), the orchestrator now generates synthetic analysis from kernel code patterns (load/store/dot counts, dimension analysis) instead of skipping the round entirely. - Added early exit on rocprof segfault/signal crashes to avoid wasting retry attempts. 3. rocprofv3 compatibility fixes: - Add --kernel-trace flag required by rocprofv3 for --stats - Add -- separator before python command in rocprof invocations - Fix target_platform kwarg leak into OptimizationWorker 4. Increase test_timeout_s from 30 to 300 seconds across all entry points (agent, manager, worker, fuser) to handle ROCm Triton JIT compilation on first run. 5. Add "rocm" to _STRATEGIES list in run_opt_manager.py. Tested: E2E MatVec on MI300X achieves 3.4x speedup (10.45ms -> 3.04ms).
Addresses feedback from @Jack-Khuu to provide a config file for out-of-the-box testing of AMD GPU optimization. Also includes Ruff formatting fixes for CI.
…ig comments - Rename 'rocm' to 'amd' in _STRATEGIES list (run_opt_manager.py) - Add --strategy amd to amd.yaml usage comment - Add --strategy nvidia to nvidia.yaml usage comment (fixes existing typo)
Signed-off-by: Andy Luo <andy.linluo@gmail.com>
FETCH_SIZE and WRITE_SIZE are derived counters that use the same internal hardware PMC block on MI300X (gfx942). When collected in the same pass, rocprofv3 hangs indefinitely or crashes with error 38: 'Request exceeds the capabilities of the hardware to collect'. Split the original 2-pass PMC collection into 4 passes: Pass 1: SQ counters (wavefront/shader utilization) — unchanged Pass 2: TCC counters (L2 cache hit/miss/EA requests) Pass 3: FETCH_SIZE (memory read bandwidth) Pass 4: WRITE_SIZE (memory write bandwidth) Verified on MI300X (8x gfx942, ROCm 7.0.2, rocprofv3 1.0.0): - All 4 passes complete successfully - Counter values are correct (tested with HIP vector_add kernel) This resolves the rocprofv3 --pmc hang reported by @kaiming-cheng.
The run_opt_manager.py CLI uses --strategy to select a config (which auto-resolves to configs/<strategy>.yaml). The README and amd.yaml usage comment incorrectly showed a --config flag that doesn't exist. Fixes feedback from @zjin-lcf in meta-pytorch#119.
Rebasing onto main left the ROCm path calling the new NCU-shaped APIs with the wrapper class and a flat rocprof payload. Class-name checks never saw ROCmKernelProfilerWrapper, so analyze(ncu_metrics=) raised and the exception was swallowed, baseline SOL stayed 0, and the new baseline_sol gate then disabled profiling for the rest of the session. The worker also dropped the multi-GPU and baseline_metrics contract main added, and a nested _raw_counters blob made flat metrics look like an NCU map and unwrap to a single scalar. Identify ROCm by the platform attribute, accept both ncu_metrics and rocm_metrics, keep profiling enabled unless ROCm itself reports that profiling is unavailable, and treat a payload as flat unless every value is a dict. The worker now round-robins GPUs, drains the result queue while joining, and forwards baseline_metrics. An omitted test timeout stays 30s on CUDA and XPU and becomes 300s only on ROCm; AutoKernelRouter keeps the YAML default of 30. ``` python -m pytest tests/test_rocm_platform.py tests/test_platform_config.py tests/test_basic.py tests/test_config_consistency.py tests/test_config_injectable.py tests/fuser/test_auto_agent_cache.py tests/test_autopatch.py -q --tb=line ```
Callers had no way to point KernelAgent at a specific rocprof. Extra YAML keys already land in OptimizationManager's worker kwargs, but the worker never unpacked them into the platform registry, and the manager's baseline profiler was built without that bag, so a configured binary would have been ignored for the profile that is actually cached. platform_kwargs is now forwarded into both registry creates. Unknown names are still dropped per factory. amd.yaml documents rocprof_bin_path as an optional override of the rocprofv3/rocprof auto-detect. rocprof_profiler.py uses native dict/list/| None, and the PMC pass notes that -i is shared by v2 and v3; only the stats pass needs --kernel-trace. ``` source /root/venv-2.13-rocm10.2/bin/activate python -m pytest tests/test_rocm_platform.py tests/test_platform_config.py tests/test_basic.py tests/test_config_consistency.py tests/test_config_injectable.py tests/fuser/test_auto_agent_cache.py tests/test_autopatch.py -q --tb=line ``` 64 passed.
rocprofv3 defaults to a rocpd database, so the stats parser never saw a CSV, and a multi-group PMC input rewrites the same counter CSV, leaving only the last group. gfx942 and gfx950 also name the EA counters TCC_EA0_* and drop a whole group when any name is missing. The stats pass now requests CSV, and each PMC group is launched on its own and merged. The parser reads the suffixed files and the long-format Counter_Name rows. A successful profile still died in the worker. Flat rocprof dicts were unwrapped with next(iter(values())), so the roofline analyzer and _finalize_results received a float. The worker also always passed the NVIDIA RooflineConfig, which has no miss_rate_amplifier. Flat metrics stay a dict, and a non-ROCm config object is ignored so the ROCm defaults apply. The Anthropic SDK no longer accepts temperature as a keyword. It is sent on the JSON body, and high_reasoning_effort sets output_config effort. The YAML wiring test uses a placeholder GPU name and rocprof path. Matvec C = A @ x, M=2048, K=1048576, BF16, claude-opus-4-6, 8 rounds, 4 workers. Starting kernels were compute-bound on the instruction mix (MI300X 83.72% compute SOL, MI350X 81.43%). MI300X, 4 GPUs: | Version | Time | |---|---| | Eager torch.matmul | 1.2031 ms | | torch.compile | 1.1794 ms | | Starting Triton kernel | 9.8626 ms | | KernelAgent best | 1.2108 ms | 8.15x vs start, 0.99x vs eager, 0.97x vs compile. MI350X, 4 GPUs: | Version | Time | |---|---| | Eager torch.matmul | 1.5973 ms | | torch.compile | 1.6011 ms | | Starting Triton kernel | 12.7146 ms | | KernelAgent best | 0.9114 ms | 13.95x vs start, 1.75x vs eager, 1.76x vs compile. RMSNorm, shape 112 x 64 x 512 x 512, BF16, same model, rounds, and worker count. MI300X, 4 GPUs: | Version | Time | |---|---| | Eager | 7.2097 ms | | torch.compile | 4.3376 ms | | Starting Triton kernel | 3.9234 ms | | KernelAgent best | 2.1854 ms | 1.80x vs start, 3.30x vs eager, 1.99x vs compile. MI350X, 4 GPUs: | Version | Time | |---|---| | Eager | 5.6796 ms | | torch.compile | 3.1459 ms | | Starting Triton kernel | 2.9189 ms | | KernelAgent best | 1.7616 ms | 1.66x vs start, 3.22x vs eager, 1.79x vs compile. ``` python -m pytest tests/test_rocm_platform.py tests/test_platform_config.py tests/test_basic.py tests/test_config_consistency.py tests/test_config_injectable.py tests/fuser/test_auto_agent_cache.py tests/test_autopatch.py -q --tb=line ```
|
Hi @Jack-Khuu, @kaiming-cheng, @zjin-lcf, |
|
@dnikolaev-amd thanks for the PR. could you fix the ci and ruff? |
OptimizationWorker always constructs Benchmark, and benchmark.py imports torch at import time. CI installs only the dev extra, so test_yaml_platform_kwargs_select_rocprof_binary failed on Ubuntu and macOS with ModuleNotFoundError. The test now inserts a stand-in Benchmark before the worker is built. The rocprof path assertions are unchanged.
|
Hi @kaiming-cheng, Ruff still fails. Unpinned |
|
Hello @dnikolaev-amd, Will you add specs for MI100 and MI210 GPUs ? |
|
Hello @dnikolaev-amd, I ran the test files listed in the test plan ( 1. Fallback On ROCm 6.4, The summary file is This is the fallback taken when 2. A missing CSV is still returned as a completed profile
3. A multi-round ROCm run raises
The ROCm analyzer sets The next access raises
|
The stop check reads early_stop on the platform analyzer.
A product name selects the row. A bare gfx90a report uses the PCI chip id, and otherwise stays MI250X.
|
Hello @zjin-lcf, MI100, MI210, and MI250 are in e7e26c7. A product name selects the row. A bare gfx90a name uses the PCI chip id, and stays MI250X when that id cannot be read. 2. A missing CSV is still returned as a completed profile:Fixed in 94b2819. A stats or PMC file that rocprof does not write now raises, including a rocprofv3 pass that exits 0 with no counter CSV. 3. A multi-round ROCm run raises AttributeError at the stop check:Fixed in 94b2819. Both analyzers expose 1. Fallback rocprof --stats does not write the -o path :Still in progress. v1 and v2 are different tools, and neither matches the rocprofv3 command.
A successful exit with the wrong filename or an empty counter file looks like a finished profile. That is the case you hit. v1 and v2 are not in the branch yet. Do we need to support the older ROCm stacks, where |
|
@dnikolaev-amd |
The worker no longer falls back to rocprof.
ROCm 10.1 puts rocprofv3 and amd-smi beside the venv Python, which is often a symlink, and does not ship rocm-smi. Listing and the MI250/MI300 chip id now use amd-smi only. The binary is taken from PATH, from beside the unresolved interpreter, then from /opt/rocm/bin. HIP ids come from `amd-smi list -e` because the tool index is not the HIP ordinal. DEVICE_ID and NUM_COMPUTE_UNITS come from `amd-smi static --asic`, which splits MI250 from MI250X on chip 0x740c. The README installs torch 2.14.0+rocm10.1.0 from stable.repo.amd.com inside a venv; that wheel installs ROCm 10.1.0 with it. Test Plan: ``` python -m pytest tests/test_rocm_platform.py -o addopts= --tb=short -q ```
|
Hello @zjin-lcf, This PR supports ROCm 10.1 and later only. Older ROCm can be used without a warranty: MI100, MI210, and MI250 are in
|
The matmul test launches a real kernel and skips in CI. The worker test only searches function source for strings. The CSV parser and GPU detection stay covered by the other tests in this file. Test Plan: ``` python -m pytest tests/test_rocm_platform.py -o addopts= --tb=short -q ```
|
Hello @dnikolaev-amd, Thanks for the update. MI100, MI210, and MI250 are in the spec table, both roofline wrappers expose if should_stop and self.roofline_analyzer.config.early_stop:ROCm Two profiler paths are still wrong. I called the functions on 1. A stats CSV with no
Input: Name,Calls,Duration(ns)
kernel,1,2000000Counter_Name,Counter_Value
SQ_INSTS_VALU,900
SQ_INSTS_SALU,100
The same call raises 2. A non-zero exit on Pass 3 is A fake The only input file for that pass is
3. MI100 FP32 The MI100 row sets |
MI350X is 256 CUs, FP32 144.2, and FP16/BF16 2.3 PF. L2 is the package total from the per-XCD and per-GCD figures, and MI300A HBM peaks at 5.3 TB/s. MI100 FP32 is 46.1, the matrix headline from the CDNA whitepaper, the same convention as MI300X. Test Plan: ``` python -m pytest tests/test_rocm_platform.py -o addopts= --tb=short -q ```
Test Plan: ``` python -m pytest tests/test_rocm_platform.py tests/test_platform_config.py tests/test_basic.py tests/test_config_consistency.py tests/test_config_injectable.py tests/fuser/test_auto_agent_cache.py tests/test_autopatch.py -q --tb=line ```
A stats CSV with no AverageNs was merged as duration_ms 0.0, and missing PMC columns were stored as 0.0. A non-zero exit on TCC_EA0_* raised before trying TCC_EA_*, so gfx90a could not finish that pass. Test Plan: ``` python -m pytest tests/test_rocm_platform.py -o addopts= --tb=short -q ```
|
Hello @zjin-lcf, Both profiler cases and the MI100 FP32 row are updated. The profiler change is 717b6dd. The spec change is 1aab87f. 1. A stats CSV with no The two CSVs from the comment now raise Missing PMC columns are left out of the result. They are not stored as 0.0. An empty PMC file raises
2. A non-zero exit on A non-zero exit now tries the next name, as an empty CSV already did. A fake
3. MI100 FP32
Every Instinct row in the table was checked against the ROCm CDNA whitepapers: MI100, MI210, MI250, MI250X, MI300A, MI300X, and MI350X. The database matches those sources. CDNA 2 FP32 stays on the vector column: MI250X 47.9, MI250 45.3, MI210 22.6. The matrix headlines there are 95.7, 90.5, and 45.3. I can switch those three if you want the headline everywhere. I am thinking of adding The suite was run again with no GPU visible: 82 passed on MI300X, MI250, and MI350. |
|
Hello @dnikolaev-amd, Thanks for the updates. I rechecked 1. A completed profile still describes every dispatch in the processThe wrapper runs the kernel three times and then once more, and both rocprofv3 commands trace that whole process. There is no kernel filter or dispatch range.
Name,Calls,TotalDurationNs,AverageNs,Percentage
at::native::vectorized_elementwise,4,8000000,2000000,80
triton_kernel,4,2000000,500000,20Dispatch_ID,Kernel_Name,Counter_Name,Counter_Value
1,triton_kernel,SQ_INSTS_VALU,100
1,triton_kernel,SQ_INSTS_SALU,100
2,triton_kernel,SQ_INSTS_VALU,100
2,triton_kernel,SQ_INSTS_SALU,100
3,at::native::vectorized_elementwise,SQ_INSTS_VALU,900
3,at::native::vectorized_elementwise,SQ_INSTS_SALU,0The result is Please keep one post-warmup dispatch of the Triton kernel, and exclude 2. An exit of 0 can reuse the previous kernel's CSVOutput names depend only on I planted The same fake binary for the stats pass, with an existing Please remove the expected stats and per-pass CSVs before each invocation, or give each kernel its own prefix, and accept a file only when that invocation created it. 3. Claude seed generation raises
|
|
Hello @dnikolaev-amd, I ran the detection and stats paths on a mixed node: AMD Instinct MI100 (HIP_ID 0, DEVICE_ID 0x738c, 120 CUs, gfx908), Tesla M40 (nvidia-smi index 0). ROCm here is 7.1.1 and
The MI100 name path is right. A real stats run confirms the dispatch aggregation. The process did |
|
Hi @zjin-lcf,
You’re the first person I know who has Nvidia and AMD GPU installed together :) Unfortunately, I don’t have that kind of setup to test with, but I’ll try to come up with something. Is this mixed node available in the CI environment so that I can run tests? |
|
Hello @dnikolaev-amd, The mixed node is not in CI. I applied the detection change from this side and checked it on the machine with the MI100 and the M40: dnikolaev-amd#1 The variable is
|
Detection returned nvidia-smi indexes and ignored HIP_VISIBLE_DEVICES and ROCR_VISIBLE_DEVICES, so a mask that hid every HIP device still scheduled a worker. CUDA_VISIBLE_DEVICES alone counted as an NVIDIA GPU, and nothing listed became GPU 0. A run uses one vendor. NVIDIA is the default when nvidia-smi lists a GPU and no ROCm mask selects a HIP id. AMD is used when nvidia-smi lists nothing, or when a HIP or ROCR mask leaves an amd-smi HIP id. An empty mask disables that vendor. A non-empty CUDA list together with a non-empty HIP or ROCR list is an error. An explicit platform that does not get its own devices raises, and nothing listed raises. A ROCm worker removes CUDA_VISIBLE_DEVICES and ROCR_VISIBLE_DEVICES and sets HIP_VISIBLE_DEVICES to that HIP id. The chip lookup reads the ASIC record from the same amd-smi listing that produced the id. Test Plan: ``` python -m pytest tests/test_rocm_platform.py -o addopts= --tb=short -q ```
A stats CSV has one row per kernel name, and AverageNs is the mean of every launch in the process, including warmup. duration_ms now comes from the kernel trace: the last dispatch of the non-at:: kernel with the largest TotalDurationNs. A later helper, such as a reduce after the main kernel, does not replace that time. A stats file with no AverageNs, or a trace with no matching dispatch, is not a profile. Test Plan: ``` python -m pytest tests/test_rocm_platform.py -o addopts= --tb=short -q ```
|
Hello @zjin-lcf, Thanks for running this on the NVIDIA and AMD GPUs, and for dnikolaev-amd#1. That machine is not in CI, so the measurements are useful. Detection is in dnikolaev-amd@e6054b0. A run uses one vendor. NVIDIA is the default when nvidia-smi lists a GPU and no HIP or ROCR mask selects an amd-smi HIP id. AMD is used when nvidia-smi lists nothing, or when the mask leaves a HIP id. An empty mask disables that vendor. HIP_VISIBLE_DEVICES=9 and ROCR_VISIBLE_DEVICES=9 select nothing, and a ROCm OptimizationManager raises instead of scheduling a worker. An explicit NVIDIA request with a matching HIP id raises as well. Nothing listed raises. There is no On a mixed node with no mask, an explicit ROCm request raises because NVIDIA devices were selected. It does not switch over to the HIP ids. amd-smi ignores both masks. The integer is a HIP id, not the HSA_ID, so ROCR_VISIBLE_DEVICES=2 hides the AMD GPU (HIP id 0). A non-empty CUDA_VISIBLE_DEVICES together with a non-empty HIP or ROCR list is an error here. On your machine HIP_VISIBLE_DEVICES=0 keeps the AMD GPU visible when CUDA_VISIBLE_DEVICES=1. This tree raises on that pair. On your machine the AMD GPU is HIP id 0 and the NVIDIA GPU is nvidia-smi index 0. Leave the other mask unset. NVIDIA: unset HIP_VISIBLE_DEVICES ROCR_VISIBLE_DEVICES
CUDA_VISIBLE_DEVICES=0 python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy nvidiaAMD, through HIP_VISIBLE_DEVICES: unset CUDA_VISIBLE_DEVICES ROCR_VISIBLE_DEVICES
HIP_VISIBLE_DEVICES=0 python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy amdAMD, through ROCR_VISIBLE_DEVICES. The value is the HIP id: unset CUDA_VISIBLE_DEVICES HIP_VISIBLE_DEVICES
ROCR_VISIBLE_DEVICES=0 python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy amdAMD, with both set. ROCR_VISIBLE_DEVICES selects HIP id 0. HIP_VISIBLE_DEVICES then indexes that list, so 0 is its first entry and the run stays on the AMD GPU: unset CUDA_VISIBLE_DEVICES
ROCR_VISIBLE_DEVICES=0 HIP_VISIBLE_DEVICES=0 python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy amdHIP_VISIBLE_DEVICES=1 with that same ROCR list is past the end, so it selects nothing. With both ROCm masks unset, unset HIP_VISIBLE_DEVICES ROCR_VISIBLE_DEVICES
python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy amdA matching AMD mask with unset CUDA_VISIBLE_DEVICES ROCR_VISIBLE_DEVICES
HIP_VISIBLE_DEVICES=0 python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy nvidia
CUDA_VISIBLE_DEVICES=1 HIP_VISIBLE_DEVICES=0 python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy amdexamples/configs/amd.yaml still sets gpu_name to The worker removes CUDA_VISIBLE_DEVICES and ROCR_VISIBLE_DEVICES and sets HIP_VISIBLE_DEVICES to the HIP id. The pin in dnikolaev-amd#1 clears the integer masks and then sets CUDA_VISIBLE_DEVICES to that id. Stats duration is in dnikolaev-amd@8cbcd64. AverageNs is the mean of every launch in the process, including the three warmup calls. duration_ms is the last kernel-trace dispatch of the non-at:: kernel with the largest TotalDurationNs. A later reduce does not replace that time, so the four add launches are no longer stored as their average. tests/test_rocm_platform.py passed. The full suite was 94 passed, 1 skipped on MI300X and on H100. The skipped test is the Blackwell oink check. |
|
Hello @dnikolaev-amd, Thanks for An explicit ROCm platform should use the HIP ids when no mask is set
nvidia_ids = _nvidia_gpu_ids()
if nvidia_ids and not rocm_names:
return "nvidia", nvidia_ids
On this machine both devices are index 0, so the failure is the raise, not a wrong integer. The MI100 is HIP id 0 and the M40 is Please keep the rest of
The change is only the requested-ROCm, no-mask path. The strategy has already chosen the vendor. The mask should restrict that list, not be required to select AMD. On a host with several AMD GPUs and one NVIDIA GPU, The counters still add every dispatch
Dispatch_ID,Kernel_Name,Counter_Name,Counter_Value
1,triton_kernel,SQ_INSTS_VALU,100
2,triton_kernel,SQ_INSTS_VALU,100
3,at::native::vectorized_elementwise,SQ_INSTS_VALU,900The merged |
A requested ROCm platform with no mask used the NVIDIA list and raised when both vendors were listed. It now reads the amd-smi HIP ids. A mask still restricts that list, and a mask that matches nothing does not switch onto NVIDIA. PMC rows were summed across every dispatch, including warmup and at:: kernels. When the CSV names kernels, each counter keeps the last Dispatch_Id of the kernel chosen for duration_ms.
|
Hello @zjin-lcf,
unset HIP_VISIBLE_DEVICES ROCR_VISIBLE_DEVICES
python examples/run_opt_manager.py \
--kernel-dir examples/optimize_01_matvec \
--strategy amdA mask that matches nothing stays empty and raises For the kernel chosen in dnikolaev-amd@8cbcd64, each PMC counter keeps the row with the greatest |
|
Hello @dnikolaev-amd, Thanks for I checked the rest of the branch against 1. GPU detection replaces the
|
| Case | main |
This PR |
|---|---|---|
platform="noop", no GPU tool |
NoOpVerifier, NoOpWorkerRunner, gpu_ids=[0] |
RuntimeError: no GPU visible |
platform="noop", nvidia-smi lists 0 and 1 |
NoOpVerifier, NoOpWorkerRunner, noop worker components |
NvidiaVerifier, NvidiaWorkerRunner, profiler: nvidia |
dict with nvidia verifier/benchmarker/worker_runner/specs_provider and noop profiler/roofline_analyzer/bottleneck_analyzer/rag_prescriber |
dict kept | every component becomes nvidia |
| no platform, no GPU tool | NVIDIA, gpu_ids=[0] |
RuntimeError: no GPU visible |
examples/configs/noop.yaml describes itself as a "dry-run without GPU hardware". It sets platform: noop and no gpu_ids. With this PR, --strategy noop raises on a host with no GPU. On an NVIDIA host it runs real verification, benchmarking, and NCU. The detection tests pass only "nvidia", "rocm", or no platform. test_yaml_platform_kwargs_select_rocprof_binary uses a noop/rocm dict, but it passes gpu_ids=[0] and skips detection. None of them catch this.
Please apply the detected vendor only when platform is None. A noop platform should not need a GPU: skip detection, or keep [0] for it. A dict that names nvidia or rocm can still be checked against the detected vendor, as now, without rewriting its other entries.
2. The ROCm "SOL" values measure instruction mix, not percent of peak
_merge_rocprof_outputs sets compute_sol_pct to the VALU share of counted instructions and memory_sol_pct to the VMEM share. ROCmRooflineAnalyzer scales the memory value by the L2 miss rate. efficiency_pct is the larger of the two. Neither value is compared with the device's peak.
Take two matvec kernels from the PR benchmark shape (M=2048, K=1048576, BF16, 4 GiB read), on MI300X (peak_memory_bw_gbps 5300). Both read the same bytes, with TCC_HIT_sum=10 and TCC_MISS_sum=90. Kernel A uses dword loads: 8M SQ_INSTS_VMEM_RD, 2M SQ_INSTS_VALU, 2.4 ms. Kernel B uses dwordx4 loads: 2M SQ_INSTS_VMEM_RD, 2M SQ_INSTS_VALU, 1.2 ms. Through _merge_rocprof_outputs and ROCmRooflineAnalyzer().analyze:
| Kernel | Time | compute_sol_pct |
memory_sol_pct |
efficiency_pct |
at_roofline |
Achieved bandwidth from FETCH_SIZE and duration_ms |
|---|---|---|---|---|---|---|
| A | 2.4 ms | 20.0 | 100.0 | 100.0 | yes | 1,790 GB/s, 33.8% of peak |
| B | 1.2 ms | 50.0 | 72.5 | 72.5 | no | 3,579 GB/s, 67.5% of peak |
The slower kernel reports 100% SOL. kernel_optimization.j2 gives the model Memory SOL: 100.0% and At Roofline: Yes for A. The judger's roofline section prints the same values. _update_kernels keeps best_sol_kernel by this number, and new_sol > best_sol_sol can set best_round_num. early_stop=False prevents a stop, but the prompts and the SOL tracking still use the number.
The profiler already collects FETCH_SIZE, WRITE_SIZE, and duration_ms, and the spec row has peak_memory_bw_gbps. Memory SOL could be (FETCH_SIZE + WRITE_SIZE) / duration divided by peak bandwidth. If the instruction-mix values stay, please keep them out of efficiency_pct, at_roofline, and the * SOL lines of the prompt templates. Report them under their real names, as _ROCM_COUNTER_FIELDS already does ("instruction mix").
3. output_config breaks the Anthropic provider on older SDKs
The temperature change is right for the current SDK. anthropic 2.4.0 has no temperature parameter on Messages.create, so extra_body is the way to send it.
The same commit adds params["output_config"] = {"effort": "high"} whenever high_reasoning_effort is set. VerificationWorker._call_llm sets it whenever the worker's high_reasoning_effort is true, and both strategy configs set high_reasoning_effort: true. pyproject.toml lists anthropic without a version. With anthropic==0.49.0:
get_response(..., high_reasoning_effort=True)
TypeError: Messages.create() got an unexpected keyword argument 'output_config'
With high_reasoning_effort=False, the same call builds and sends the request. This change affects every Claude user, not only ROCm. Please move it to a separate PR. There, either pin a minimum anthropic version that accepts output_config, or send effort through extra_body like temperature.
4. CUDA no longer profiles the final best kernel when its SOL profile failed
The final roofline pass now requires best_ncu_metrics is not None:
if best_round_num > 0 and best_ncu_metrics is not None:best_ncu_metrics is set only from new_kernel_metrics. If _profile_kernel_for_sol returns None (a transient NCU failure), a round can still become the best by runtime, and then the final pass is skipped. I ran optimize_kernel on an NVIDIA-style profiler (no platform attribute) for one round. The new kernel benchmarks at 1.0 ms against a 2.0 ms baseline, and _profile_kernel_for_sol returns None:
best_round |
profile_kernel(kernel_round_1.py) for the final roofline |
|
|---|---|---|
main |
1 | called once |
| This PR | 1 | not called |
_finalize_results then gets best_ncu_metrics=None on CUDA. Please keep the old condition on NVIDIA and skip the final pass only when profiler_is_rocm(self.profiler) and the session switched to synthetic mode.
Smaller items
- The per-round roofline log is now wrapped in
except Exception: pass. On CUDA, that hides any NCU roofline error. The comment says the block is a no-op on ROCm, but it runs there too. Please catch only the expected case, or log the exception. - The synthetic
BottleneckResulttext appears three times inoptimization_orchestrator.py. One helper would keep the three copies the same. ROCmWorkerRunnerand_rocm_worker_processcopyNvidiaWorkerRunnerand its worker function except for the device pin andtarget_platform. A shared runner with a pin hook would keep the two in sync.- On ROCm,
OptimizationManager.__init__callsos.environ.pop("CUDA_VISIBLE_DEVICES")in the manager process. That changes the environment of whatever process created the manager. Workers already remove it in_pin_rocm_visible_devices. # A missing baseline SOL used to force synthetic mode, which also disabled NCU on CUDAdescribes the Add ROCm/AMD GPU support for kernel generation and optimization #119 branch.mainnever had that behavior.
Please keep the mask rules, visible_hip_ids, the CUDA/ROCm mask conflict check, the worker pin, the platform-specific timeout default, and platform_kwargs.rocprof_bin_path reaching both the manager's baseline profiler and the workers.
Summary
This branch continues meta-pytorch/KernelAgent#119 and is rebased onto current
main. #119 is the base for the ROCm/AMD path:--target-platform rocm, rocprof profiling in place of NCU, heuristic roofline analysis, Instinct GPU specs (MI300X, MI300A, MI350X, MI250X, MI250, MI210, MI100), and platform-registry components underrocm.The full #119 conversation was read, and every requested change from that thread is applied here. Further changes came from testing and benchmarking on MI300X, MI350X, and H100. The commit history has the per-change detail.
ROCm 10.1 is the supported floor. Profiling uses
rocprofv3only. GPU listing and the MI250/MI300 chip id useamd-smionly.rocproforrocprofv2. That removes the ROCm 6.4rocprof --statspath, which reported a completed profile with empty counters.amd-smiis taken from$PATH, from beside the unresolved Python (a venv interpreter is often a symlink whose target has no siblingamd-smi), then from/opt/rocm/bin. HIP ids come fromamd-smi list -e.DEVICE_IDandNUM_COMPUTE_UNITScome fromamd-smi static --asic.torch==2.14.0+rocm10.1.0fromstable.repo.amd.cominside a venv. That wheel installs ROCm 10.1.0 with it, includingrocprofv3andamd-smibeside that Python.nvidia-smilists a GPU and no HIP or ROCR mask selects an amd-smi HIP id. AMD is used whennvidia-smilists nothing, when the mask leaves a HIP id, or when a ROCm platform was requested and both masks are unset. An empty mask disables that vendor. A non-emptyCUDA_VISIBLE_DEVICEStogether with a non-emptyHIP_VISIBLE_DEVICESorROCR_VISIBLE_DEVICESis an error. An explicit platform that does not get its own devices raises, and nothing listed raises. A ROCm worker removesCUDA_VISIBLE_DEVICESandROCR_VISIBLE_DEVICESand setsHIP_VISIBLE_DEVICESto that HIP id. The integer in both ROCm masks is the HIP id, the same index as unmaskedcuda:N, and the chip lookup reads the ASIC record from the same amd-smi listing that produced the id.duration_msis the last kernel-trace dispatch of the non-at::kernel with the largestTotalDurationNs.AverageNsis the mean of every launch in the process, including warmup. A later helper, such as a reduce after the main kernel, does not replace that time. A stats file with noAverageNs, or a trace with no matching dispatch, is not a profile. When a PMC file names kernels, each counter keeps the greatestDispatch_Idof that same kernel.at::rows are left out. A file with no kernel column still adds every value.Review requests from #119
examples/configs/amd.yamlfor out-of-the-box--strategy amdruns (Jack Khuu). Strategy selection discovers configs by filename, so the strategy name isamd.--strategy amdand--strategy nvidia. The--configflag is not documented, matching the CLI (Jack Khuu, Z Jin).rocprofv3,amd-smi, and theamdquickstart (Jack Khuu).platform_kwargs.rocprof_bin_pathselects the profiler binary, on the same pattern asncu_bin_path. The manager baseline profiler and the workers both receive it (Jack Khuu). The binary isrocprofv3.rocprof_profiler.pyusesdict,list, and| None. Each PMC group is its ownrocprofv3 -ipass. The stats pass adds--kernel-traceand--output-format csv(Jack Khuu).Changes from testing and benchmarking
--pmcno longer hangs on gfx942.FETCH_SIZEandWRITE_SIZEshare a PMC block, so each counter group is its own pass. gfx942 and gfx950 useTCC_EA0_*names. gfx90a uses unprefixedTCC_EA_*. Stats output is CSV, and the parser reads suffixed files and long-formatCounter_Namerows (Kaiming Cheng). A pass that exits 0 but writes no CSV is not a completed profile. A missing PMC column is omitted rather than stored as 0. A non-zero exit or an empty CSV onTCC_EA0_*triesTCC_EA_*.specs_confidence. L2 is the package total: 4 MB per XCD on CDNA 3 and 4 (MI300X and MI350X 32, MI300A 24) and 8 MB per GCD on CDNA 2 (MI250 and MI250X 16, MI210 8). MI300A peak bandwidth is 5.3 TB/s. MI100 FP32 is 46.1, the matrix headline, the same convention as MI300X. CDNA 2 stays on the vector FP32 numbers.warp_specialize. CUDA kernel guidance is empty.main, the ROCm path uses the platform attribute, accepts bothncu_metricsandrocm_metrics, and keeps a flat rocprof dict intact. A SOL of 0 does not turn profiling off for the rest of the session. Workers round-robin GPUs, drain the result queue while joining, and forward cachedbaseline_metrics.temperatureon the JSON body, andhigh_reasoning_effortsets output-config effort.ROCm platform was requested but no devices are visible. An explicit NVIDIA request with a matching HIP id raisesNVIDIA platform was requested but ROCm devices were selected.Z Jin's later note about illegal addresses in KernelBench level-1 SDPA at 20 rounds on MI300A was read. It asks whether the failure reproduces and does not request a code change. This branch does not include a fix for that report.
Benchmarks
Matvec
C = A @ x, M=2048, K=1048576, BF16. RMSNorm, shape 112 x 64 x 512 x 512, BF16. All three runs used claude-opus-4-6, 8 rounds, and 4 workers.Matvec
torch.matmultorch.compileMI300X: 8.15x vs start, 0.99x vs eager, 0.97x vs compile. MI350X: 13.95x vs start, 1.75x vs eager, 1.76x vs compile. H100: 3.51x vs start, 1.09x vs eager, 1.11x vs compile.
RMSNorm
torch.compileMI300X: 1.80x vs start, 3.30x vs eager, 1.99x vs compile. MI350X: 1.66x vs start, 3.22x vs eager, 1.79x vs compile. H100: 1.01x vs start, 2.15x vs eager, 1.40x vs compile.
Tests
One new file,
tests/test_rocm_platform.py. No existing test file was changed.test_registry_lists_rocm_for_every_componenttest_registry_profiler_is_the_wrappertest_ncu_bin_path_is_accepted_as_rocprof_binarytest_roofline_accepts_shared_ncu_metrics_keywordtest_roofline_rejects_scalar_metricstest_flat_rocprof_metrics_are_not_unwrapped_to_a_scalartest_synthetic_mode_is_rocm_onlytest_timeout_default_is_platform_specifictest_gfx_isa_names_map_onto_spec_database_keystest_gfx90a_chip_id_separates_mi210_and_mi250test_combined_mi250_name_uses_chip_idtest_amd_smi_asic_follows_hip_idtest_gfx942_chip_id_separates_mi300a_and_mi300xtest_prompt_uses_cu_labelstest_rocm_kernel_guidance_skips_sku_numberstest_optimization_prompt_uses_cu_labels_for_cdnatest_refinement_prompt_includes_rocm_kernel_guidancetest_bottleneck_analyzer_accepts_num_bottleneckstest_yaml_platform_kwargs_select_rocprof_binarytest_missing_rocprof_csv_is_not_a_profiletest_collected_rocprof_csvs_keep_measured_counterstest_duration_is_the_last_triton_dispatchtest_pmc_keeps_the_last_dispatch_of_the_profiled_kerneltest_stats_csv_without_kernel_trace_is_not_a_profiletest_stats_csv_without_averagens_is_not_a_profiletest_rocprofv3_pass_without_csv_is_not_collectedtest_rocprofv3_uses_long_output_formattest_rocprofv3_retries_ea_counter_after_nonzero_exittest_roofline_wrappers_expose_early_stoptest_find_amd_smi_beside_python_before_resolved_pathtest_find_amd_smi_uses_opt_rocm_bintest_mixed_node_uses_nvidia_unless_hip_visible_devicestest_path_binaries_use_nvidia_unless_hip_visible_devicestest_no_listed_gpu_stopstest_amd_smi_print_order_is_kept_when_nvidia_is_absenttest_unmatched_hip_visible_devices_disables_amdtest_pin_rocm_worker_unsets_cuda_visible_devicestest_visible_hip_id_uses_the_detected_listtest_chip_lookup_uses_the_listing_that_produced_the_hip_idtest_manager_runs_the_vendor_the_visibility_rules_choosetest_cuda_visible_devices_without_nvidia_smi_keeps_amdtest_explicit_nvidia_without_listed_gpus_does_not_invent_oneTest plan
python -m pytest tests -q --tb=line -o addopts=withCUDA_VISIBLE_DEVICES,HIP_VISIBLE_DEVICES, andROCR_VISIBLE_DEVICESunset (95 passed, 1 skipped locally). The skip istest_oink_rmsnorm_sm100_correctness.CUDA_VISIBLE_DEVICES=0,1and the two ROCm masks unset (95 passed, 1 skipped). The skip is the same oink test.FETCH_SIZEandWRITE_SIZEin separate groupsRuff is unchanged. Unpinned
ruff check .still fails on the existing tree, and this push does not fix that.