Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions mlir/test/perf-scripts/runtime/tuningRunner-gemm.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Using a tiny GEMM with --debug-quick-tune-data. This emits a `.debug`
# TSV of the per-config table entries (PerfConfig + TFlops) but, unlike the
# full ``--debug`` flag, omits the heavy per-iteration ``MeasurementsMs``
# arrays. Verify the debug file is produced, has the expected header and
# per-config rows, and that the measurements column is absent.
# full ``--debug`` flag, omits the per-config timing statistics in ``Stats``.
# Verify the debug file is produced, has the expected header and per-config
# rows, and that the statistics column is absent.
#
# tuningRunner.py drives real GPU tuning, so it needs the ROCm runner / GPU
# runtime.
Expand All @@ -11,7 +11,7 @@
# RUN: tuningRunner.py --op gemm --tuning-space=quick --debug-quick-tune-data \
# RUN: --config='-g 1 -m 64 -n 64 -k 64 -t f32 -out_datatype f32 -transA 0 -transB 0' \
# RUN: -q -o %t2.tsv
# RUN: FileCheck %s --check-prefix=DEBUG --implicit-check-not=MeasurementsMs < %t2.tsv.debug
# RUN: FileCheck %s --check-prefix=DEBUG --implicit-check-not=Stats < %t2.tsv.debug
#
# DEBUG: PerfConfig{{.*}}TFlops
# DEBUG: {{v[0-9]+:}}
78 changes: 70 additions & 8 deletions mlir/tools/rocmlir-tuning-driver/rocmlir-tuning-driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/Signals.h"
#include "llvm/Support/SourceMgr.h"
#include "llvm/Support/ThreadPool.h"
#include "llvm/Support/raw_ostream.h"

#include <atomic>
#include <cassert>
Expand All @@ -57,6 +59,7 @@
#include <mutex>
#include <optional>
#include <thread>
#include <unistd.h>

#include "CacheFlush.h"
#include "ConcurrentQueue.h"
Expand Down Expand Up @@ -192,12 +195,16 @@ static llvm::cl::opt<unsigned> sleepUs(
static llvm::cl::opt<bool> showStats(
"show-stats",
llvm::cl::desc(
"Print detailed stats (min, max, median, stddev, cv) in JSON format."),
"Print detailed stats (min, max, median, stddev, cv) in nanoseconds as "
"JSON. In case of small kernels print total_cpu_time and number of "
"iterations."),
llvm::cl::init(false));

static llvm::cl::opt<bool> showAllMeasurements(
"show-all-measurements",
llvm::cl::desc("Print all individual timing measurements in JSON format."),
llvm::cl::desc(
"Print all individual timing measurements in nanoseconds as JSON. In "
"case of small kernels print total_cpu_time and number of iterations."),
llvm::cl::init(false));

static llvm::cl::opt<std::string> benchmarkConfig(
Expand Down Expand Up @@ -827,9 +834,16 @@ benchmarkKernels(ArrayRef<std::string> binaries,
return failure();
}

constexpr auto msToNs = [](double ms) { return 1e6 * ms; };

// Convert measurements from milliseconds to nanoseconds
for (double &measurement : measurements) {
measurement = msToNs(measurement);
}

if (params.showAllMeasurements) {
if (isSmallKernel) {
llvm::outs() << "{\"total_cpu_time\":" << smallKernelCpuMs
llvm::outs() << "{\"total_cpu_time\":" << msToNs(smallKernelCpuMs)
<< ",\"iterations\":" << smallKernelIters << "}\t";
} else {
llvm::outs() << "[";
Expand All @@ -848,7 +862,7 @@ benchmarkKernels(ArrayRef<std::string> binaries,
// The legacy small-kernel path uses a single CPU timer, so only the
// aggregate CPU time is available (no per-run min/max/median).
if (isSmallKernel) {
llvm::outs() << "{\"total_cpu_time\":" << smallKernelCpuMs
llvm::outs() << "{\"total_cpu_time\":" << msToNs(smallKernelCpuMs)
<< ",\"iterations\":" << smallKernelIters << "}\t";
}
if (measurements.size() > 1) {
Expand All @@ -864,11 +878,10 @@ benchmarkKernels(ArrayRef<std::string> binaries,
}
}

auto msToNs = [](double ms) { return 1e6 * ms; };
if (params.useMedian)
return msToNs(computeMedian(measurements));
return computeMedian(measurements);
else
return msToNs(computeMean(trimValues(measurements, params.trimPercent)));
return computeMean(trimValues(measurements, params.trimPercent));
}

static int toKernelOrder(Attribute attr) {
Expand Down Expand Up @@ -899,6 +912,46 @@ static LogicalResult extractFuncOps(ModuleOp op,
return success();
}

/// Perf config the calling thread is compiling, or null when it is between
/// configs. Points into the config list, which outlives the workers.
static thread_local const SmallString<64> *compilingConfig = nullptr;

/// Writes to stderr without llvm::errs(), which is neither async-signal-safe
/// nor safe to reenter from a fatal error handler. Give up silently on a failed
/// write: there is nothing left to report it with.
static void writeToStderr(StringRef message) {
ssize_t written = ::write(STDERR_FILENO, message.data(), message.size());
(void)written;
}

/// Names the perf config being compiled on the crashing thread. Clears the
/// pointer so that the abort() following report_fatal_error() does not report
/// the same config twice through the signal handler.
static void reportCompilingConfig() {
if (!compilingConfig)
return;
SmallString<256> message;
{
llvm::raw_svector_ostream os(message);
os << "Offending perf config: " << *compilingConfig << "\n"
<< "Reproduce with `--benchmark-config=" << *compilingConfig << "`\n";
}
writeToStderr(message);
compilingConfig = nullptr;
}

static void compilationFatalErrorHandler(void *, const char *reason, bool) {
SmallString<256> message;
{
llvm::raw_svector_ostream os(message);
os << "LLVM ERROR: " << reason << "\n";
}
writeToStderr(message);
reportCompilingConfig();
}

static void compilationSignalHandler(void *) { reportCompilingConfig(); }

static bool doesModuleHaveFusions(ModuleOp module) {
WalkResult result = module.walk([](Operation *op) {
// Check for linalg.generic or rock.reduce (standalone fusion ops)
Expand Down Expand Up @@ -1229,7 +1282,11 @@ static LogicalResult runTuningLoop(ModuleOp source) {
if (idx >= configs.size())
break;

if (!compilationResults.push(compileConfig(idx, myRes)))
compilingConfig = &configs[idx];
CompilationResult result = compileConfig(idx, myRes);
compilingConfig = nullptr;

if (!compilationResults.push(std::move(result)))
break; // Queue terminated
}

Expand Down Expand Up @@ -1313,6 +1370,11 @@ static LogicalResult runTuningLoop(ModuleOp source) {
int main(int argc, char **argv) {
llvm::InitLLVM y(argc, argv);

// Name the perf config under compilation if a backend pass in the pipeline
// dies, either through report_fatal_error or a crash signal.
llvm::install_fatal_error_handler(compilationFatalErrorHandler);
llvm::sys::AddSignalHandler(compilationSignalHandler, nullptr);

mlir::registerMLIRCLOptions();
llvm::cl::ParseCommandLineOptions(argc, argv, "rocMLIR tuning driver");

Expand Down
10 changes: 10 additions & 0 deletions mlir/utils/performance/perfRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@ def chip_has_mfma():
return has_feature(lookup_arch_info(get_chip()).default_features, GemmFeatures.MFMA)


def chip_uses_wgp_mode(chip: Optional[str] = None) -> bool:
"""Whether the chip is built in workgroup processor (WGP) mode.

In WGP mode HIP counts one multiprocessor per two-CU workgroup processor, so the CU
count it reports is half of the physical one that rocminfo prints. Wave size is the
same proxy for WGP mode that fixNaviProperties() uses in AmdArchDb.cpp.
"""
return lookup_arch_info(chip or get_chip()).wave_size == 32


DATA_TYPES_ATTENTION = None


Expand Down
25 changes: 25 additions & 0 deletions mlir/utils/performance/tests/test_perfRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,31 @@ def fake_lookup(arch):
assert captured["arch"] == "native:1"


class TestChipUsesWgpMode:
"""Tests for chip_uses_wgp_mode (wave size proxy for WGP mode)."""

def test_wave64_chip_is_not_wgp_mode(self, monkeypatch):
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=64))
assert not perfRunner.chip_uses_wgp_mode("gfx942")

def test_wave32_chip_is_wgp_mode(self, monkeypatch):
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=32))
assert perfRunner.chip_uses_wgp_mode("gfx1100")

def test_defaults_to_current_chip(self, monkeypatch):
captured = {}

def fake_lookup(arch):
captured["arch"] = arch
return types.SimpleNamespace(wave_size=32)

monkeypatch.setattr(perfRunner, "lookup_arch_info", fake_lookup)
assert perfRunner.chip_uses_wgp_mode()
assert captured["arch"] == "gfx900"


class TestParseDataTypes:
"""Tests for parse_data_types (gemm data types)."""

Expand Down
118 changes: 118 additions & 0 deletions mlir/utils/performance/tests/test_tuningRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import tempfile
import threading
import time
import types
from pathlib import Path

import pytest
Expand Down Expand Up @@ -215,6 +216,58 @@ def test_running_becomes_crashed_on_load(self):
if os.path.exists(path):
os.unlink(path)

def _ctx_key(self, num_cu=None):
return f"{self._ARCH}/{num_cu or self._NUM_CU}/{self._NUM_CHIPLETS}/full"

def _write_state_file(self, contexts):
with tempfile.NamedTemporaryFile(mode="w", suffix=".state", delete=False) as f:
f.write(json.dumps({"contexts": contexts}))
return f.name

def test_legacy_wgp_context_is_merged_and_migrated(self, monkeypatch):
"""A context keyed by the doubled CU count belongs to this device on WGP-mode chips."""
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=32))
legacy_key = self._ctx_key(2 * self._NUM_CU)
path = self._write_state_file({
legacy_key: {
self._TV_A: "crashed",
self._TV_B: "failed"
},
self._ctx_key(): {
self._TV_A: "timed_out"
},
})
try:
sf = self._make_state_file(path)
# The active context wins where the two overlap
assert sf.state.configs.get(self._TV_A) == ConfigState.TIMED_OUT
assert sf.state.configs.get(self._TV_B) == ConfigState.FAILED

with open(path, 'r') as f:
contexts = json.load(f)["contexts"]
assert legacy_key not in contexts
assert contexts[self._ctx_key()] == {self._TV_A: "timed_out", self._TV_B: "failed"}
finally:
if os.path.exists(path):
os.unlink(path)

def test_doubled_cu_context_untouched_in_cu_mode(self, monkeypatch):
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=64))
other_key = self._ctx_key(2 * self._NUM_CU)
path = self._write_state_file({other_key: {self._TV_A: "failed"}})
try:
sf = self._make_state_file(path)
assert sf.state.is_empty()

with open(path, 'r') as f:
contexts = json.load(f)["contexts"]
assert contexts == {other_key: {self._TV_A: "failed"}}
finally:
if os.path.exists(path):
os.unlink(path)

def test_old_state_file_configs_are_canonicalized(self):
"""Non-canonical test vectors in state file are canonicalized on load."""
non_canonical = "-g 1 -m 1024 -k 769 -n 512 -t f32 -out_datatype f32 -transA false -transB false"
Expand Down Expand Up @@ -296,6 +349,53 @@ def test_parse_new_format_tsv(self):
finally:
os.unlink(path)

def _write_gemm_tsv(self, tv, num_cu, arch="gfx1100"):
with tempfile.NamedTemporaryFile(mode="w", suffix=".tsv", delete=False) as f:
f.write(
"# arch\tnumCUs\tnumChiplets\ttestVector\tperfConfig\tTFlops\ttuningSpace\tcommitId\ttimestamp\tdurationSec\n"
)
f.write(
f"{arch}\t{num_cu}\t1\t{tv}\tperf_best\t1.5\tfull\tabc123\t2025-01-01T00:00:00Z\t10.0\n"
)
return f.name

def test_legacy_doubled_num_cu_loaded_in_wgp_mode(self, monkeypatch):
tv = "-t f32 -out_datatype f32 -transA false -transB false -g 1 -m 1024 -n 512 -k 769"
path = self._write_gemm_tsv(tv, num_cu=128)
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=32))
try:
opts = self._options(path, arch="gfx1100", num_cu=64)
cache = TunedConfigsCache.from_output_file(opts, GemmConfiguration)
assert cache.count() == 1
assert cache.contains(tv)
finally:
os.unlink(path)

def test_doubled_num_cu_not_loaded_in_cu_mode(self, monkeypatch):
tv = "-t f32 -out_datatype f32 -transA false -transB false -g 1 -m 1024 -n 512 -k 769"
path = self._write_gemm_tsv(tv, num_cu=128, arch="gfx942")
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=64))
try:
opts = self._options(path, arch="gfx942", num_cu=64)
cache = TunedConfigsCache.from_output_file(opts, GemmConfiguration)
assert cache.count() == 0
finally:
os.unlink(path)

def test_unrelated_num_cu_not_loaded_in_wgp_mode(self, monkeypatch):
tv = "-t f32 -out_datatype f32 -transA false -transB false -g 1 -m 1024 -n 512 -k 769"
path = self._write_gemm_tsv(tv, num_cu=96)
monkeypatch.setattr(perfRunner, "lookup_arch_info",
lambda arch: types.SimpleNamespace(wave_size=32))
try:
opts = self._options(path, arch="gfx1100", num_cu=64)
cache = TunedConfigsCache.from_output_file(opts, GemmConfiguration)
assert cache.count() == 0
finally:
os.unlink(path)

def test_arch_mismatch_not_loaded(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".tsv", delete=False) as f:
f.write(
Expand Down Expand Up @@ -557,6 +657,24 @@ def test_tuning_space_choices(self):
)
assert parsed.tuning_space == "quick"

def _parse_output(self, config_args):
topology = _make_mock_gpu_topology([(0, "gfx900")])
parsed = tuningRunner.parse_arguments(topology, [0], ["--op", "gemm"] + config_args)
return parsed.output

def test_output_defaults_to_configs_file_plus_tsv(self):
derived = self._parse_output(["-c", "configs/tier1-gemm-configs"])
assert derived == "configs/tier1-gemm-configs.tsv"

def test_explicit_output_overrides_configs_file_name(self):
explicit = self._parse_output(["-c", "configs/gemm.txt", "-o", "/tmp/out.tsv"])
assert explicit == "/tmp/out.tsv"

def test_output_falls_back_without_a_configs_file(self):
single_config = self._parse_output(["--config", "-g 1 -m 1024 -k 769 -n 512"])
assert single_config == tuningRunner.DEFAULT_OUTPUT_FILE
assert self._parse_output(["-c", "-"]) == tuningRunner.DEFAULT_OUTPUT_FILE

def test_negative_gpu_run_timeout_rejected(self, capsys):
topology = _make_mock_gpu_topology([(0, "gfx900")])
available = [0]
Expand Down
Loading
Loading