diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py new file mode 100644 index 000000000..c4e18647a --- /dev/null +++ b/docs/tutorials/descript++_notebook.py @@ -0,0 +1,811 @@ +import marimo + +__generated_with = "0.20.0" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + # === Utility functions for the tutorial === + from io import StringIO + from contextlib import redirect_stderr, redirect_stdout + import traceback + from typing import Any + import queue + import time + import multiprocessing as mp + import exec_utils + import matplotlib.pyplot as plt + + def get_backend_import(backend_name: str) -> str: + """Return the import statement for the selected backend.""" + if backend_name == "MLIR": + return "from xtc.backends.mlir import Backend" + else: + return "from xtc.backends.tvm import Backend" + + def get_print_opts_str(output_option: str) -> str: + """Return the compiler print options string for the selected output.""" + opts_map = { + "Source IR": "print_source_ir=True", + "Transformed IR": "print_transformed_ir=True", + "Lowered IR": "print_lowered_ir=True", + "Assembly": "print_assembly=True", + } + return opts_map.get(output_option, "") + + def get_print_opts_dict(output_option: str) -> dict: + """Return the compiler print options as a dictionary.""" + opts_map = { + "Source IR": {"print_source_ir": True}, + "Transformed IR": {"print_transformed_ir": True}, + "Lowered IR": {"print_lowered_ir": True}, + "Assembly": {"print_assembly": True}, + } + return opts_map.get(output_option, {}) + + def get_backend_class(backend_name: str): + """Return the Backend class for the selected backend.""" + if backend_name == "MLIR": + from xtc.backends.mlir import Backend + else: + from xtc.backends.tvm import Backend + return Backend + + def get_output_options(backend_name: str) -> list: + """Return available output options based on backend (TVM doesn't support Lowered IR).""" + if backend_name == "TVM": + return ["Source IR", "Transformed IR", "Assembly"] + else: + return ["Source IR", "Transformed IR", "Lowered IR", "Assembly"] + + def create_backend_radio(label: str = "Backend:"): + """Create a radio button for backend selection.""" + return mo.ui.radio(options=["MLIR", "TVM"], value="TVM", label=label) + + def create_output_radio(backend_name: str, label: str = "Output options:"): + """Create a radio button for output options based on backend.""" + return mo.ui.radio( + options=get_output_options(backend_name), + value="Assembly", + label=label + ) + + def execute_editor_code(editor_value: str, display_results_fn=None, initial_namespace=None): + """ + Execute editor code with stdout/stderr capture. + Returns (success, output, captured_data). + - If display_results_fn is provided, it's injected as 'display_results' in the namespace. + - If initial_namespace is provided, those values are injected before execution. + - captured_data contains any data captured via display_results. + """ + captured = {"perf": 0.0} + + def _display_results(perf): + captured["perf"] = perf + + namespace = dict(initial_namespace) if initial_namespace else {} + if display_results_fn is not None: + namespace["display_results"] = _display_results + + code_stderr = StringIO() + code_stdout = StringIO() + + try: + with redirect_stderr(code_stderr), redirect_stdout(code_stdout): + exec(editor_value, namespace) + output = code_stderr.getvalue() + code_stdout.getvalue() + return True, output, captured + except Exception: + return False, traceback.format_exc(), captured + + def render_editor_output(success: bool, output: str, captured: dict): + """Render the output of an editor execution as marimo elements.""" + if not success: + return mo.md(f"**Code error:**\n```\n{output}\n```") + + perf_display = mo.md(f"**Performance:** {captured['perf']:.2f}% of peak") + code_content = mo.md(f"```asm\n{output}\n```") if output else mo.md("*No IR output.*") + code_accordion = mo.accordion({"Generated Code": code_content}) + return mo.vstack([perf_display, code_accordion]) + + def run_exploration(generator, get_info=None): + """ + Run exploration with progress bar. Returns sorted results. + Generator should yield (index, total, sample_or_name, perf) tuples. + """ + results = [] + best_sample = None + best_perf = 0.0 + captured_info = {} + + first_result = next(generator, None) + if first_result is None: + return [], {} + + idx, total, sample, perf = first_result + results.append({"sample": sample, "perf": perf}) + if perf > best_perf: + best_perf = perf + best_sample = sample + + with mo.status.progress_bar(total=total, title="Exploring schedules...", remove_on_exit=False) as progress: + progress.update( + title=f"Exploring schedules... (best: {best_perf:.1f}%)", + subtitle=f"Sample {idx + 1}/{total}: {sample} -> {perf:.1f}%" + ) + + for idx, total, sample, perf in generator: + results.append({"sample": sample, "perf": perf}) + + if perf > best_perf: + best_perf = perf + best_sample = sample + + progress.update( + title=f"Exploring schedules... (best: {best_perf:.1f}%)", + subtitle=f"Sample {idx + 1}/{total}: {sample} -> {perf:.1f}%" + ) + + if get_info: + captured_info.update(get_info()) + + sorted_results = sorted(results, key=lambda x: x["perf"], reverse=True) + return sorted_results, captured_info, results + + def start_streaming_execution( + *, + code: str, + out: Any, + throttle_s: float = 0.5, + ) -> Any: + """ + Start a marimo Thread that launches a subprocess and streams its output. + Cancellation: if the spawning cell is invalidated, thread.should_exit becomes True, + and we terminate the subprocess. + """ + def target(): + import marimo as mo + thread = mo.current_thread() + + ctx = mp.get_context("spawn") + out_q: "mp.Queue" = ctx.Queue() + p = ctx.Process( + target=exec_utils._child_exec, + args=(code, out_q), + daemon=True + ) + p.start() + + buf: list[str] = [] + last = 0.0 + + def render(force: bool = False): + nonlocal last + now = time.time() + if force or (now - last) >= throttle_s: + out.replace( + mo.md( + f"**Output:**\n\n```text\n{''.join(buf)}\n```" + ) + ) + last = now + + try: + out.replace(mo.md("**Output:**\n\n```text\n\n```")) + render(force=True) + + while True: + # Cancel requested? (cell invalidated by Cancel click / rerun / interrupt) + if thread.should_exit: + buf.append("\n[Cancelled]\n") + render(force=True) + if p.is_alive(): + p.terminate() + p.join(timeout=1) + break + + # Process ended? + if not p.is_alive(): + # Drain remaining queue chunks + while True: + try: + kind, payload = out_q.get_nowait() + except queue.Empty: + break + if kind == "chunk": + buf.append(payload) + render(force=True) + break + + # Get output chunk (non-blocking-ish) + try: + kind, payload = out_q.get(timeout=0.1) + except queue.Empty: + continue + + if kind == "chunk": + buf.append(payload) + render() + elif kind == "done": + # allow loop to observe process exit / drain remaining data + continue + finally: + if p.is_alive(): + p.terminate() + p.join(timeout=1) + + t = mo.Thread(target=target, daemon=True) + t.start() + return t + + return ( + create_backend_radio, + create_output_radio, + execute_editor_code, + get_backend_class, + get_print_opts_dict, + plt, + render_editor_output, + run_exploration, + start_streaming_execution, + traceback, + ) + + +@app.cell +def _(mo): + mo.md(r""" + # Descript + + XTC allows you to describe target loop structures for operators using a small DSL called `descript`. Instead of manually specifying each transformation step, you declare the desired final loop structure, and XTC automatically infers the sequence of transformations needed to achieve it. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Define a schedule declaratively + + `descript` can be used to describe a completed loop structure, with annotations marking optimisations. + + For example, the following loop structure: + ``` + for i in ... // parallelized + for k in ... + for j in ... + for i1 in range(8): // unrolled + for j1 in range(16): // vectorized + ``` + + Can be described as: + ```python + ''' + i: parallelize + k: + j: + i#8: unroll + j#16: vectorize + ''' + ``` + + The loop order follows the given structure (outer to inner), `j#16` creates a tile of size 16 on `j`, and the `vectorize` attribute marks that inner loop for vectorization. Similarly, `parallelize` and `unroll` mark their respective loops for parallelization and (full) unrolling. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **Practice.** The code below lets you define a schedule specification and see the generated assembly. Try replicating the good-enough schedule you discovered in the previous section! + """) + return + + +@app.cell +def _(mo): + + descript_spec = mo.ui.code_editor( + value='''# Schedule specification + schedule_spec = """ + i: parallelize + k: + j: + i#8: unroll + j#16: vectorize + """''', + language="python", + label="") + descript_editor = mo.ui.code_editor( + value='''import xtc.graphs.xtc.op as O + from xtc.graphs.xtc.graph import XTCGraph + from xtc.schedules.descript import descript_scheduler + from xtc.runtimes.host import HostRuntime + + rt = HostRuntime.get() + + # Problem setup + I, J, K, dtype = 16, 32, 512, "float32" + + def matmul_graph(I: int, J: int, K: int, dtype: str) -> XTCGraph: + """Create a graph computing C = A @ B.""" + a = O.tensor((I, K), dtype, name="A") + b = O.tensor((K, J), dtype, name="B") + with O.graph(name="matmul") as gb: + O.matmul(a, b, name="C") + return gb.graph + + graph = matmul_graph(I, J, K, dtype) + backend = Backend(graph) + + # Compile + scheduler = backend.get_scheduler() + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + abstract_matrix=["A", "B", "C"], + spec=schedule_spec + ) + schedule = scheduler.schedule() + + compiler = backend.get_compiler(dump_file="matmul", shared_lib=True, **print_opts) + module = compiler.compile(schedule) + + # Evaluate and display results + peak_flops = rt.evaluate_flops(dtype) + evaluator = module.get_evaluator() + results, _, _ = evaluator.evaluate() + perf = (I * J * K) / min(results) / peak_flops * 100 + + display_results(perf) + ''', + language="python", + label="" + ) + mo.vstack([descript_spec, descript_editor]) + return descript_editor, descript_spec + + +@app.cell +def _(create_backend_radio): + descript_backend_radio = create_backend_radio() + return (descript_backend_radio,) + + +@app.cell +def _(create_output_radio, descript_backend_radio, mo): + descript_output_radio = create_output_radio(descript_backend_radio.value) + mo.hstack([descript_backend_radio, descript_output_radio], justify="start", gap=4) + return (descript_output_radio,) + + +@app.cell +def _( + descript_backend_radio, + descript_editor, + descript_output_radio, + descript_spec, + execute_editor_code, + get_backend_class, + get_print_opts_dict, + mo, + render_editor_output, +): + _namespace = { + "Backend": get_backend_class(descript_backend_radio.value), + "print_opts": get_print_opts_dict(descript_output_radio.value), + } + exec(descript_spec.value, _namespace) + _success, _output, _captured = execute_editor_code(descript_editor.value, display_results_fn=True, initial_namespace=_namespace) + mo.stop(not _success, mo.md(f"**Code error:**\n```\n{_output}\n```")) + render_editor_output(_success, _output, _captured) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Experimenting with Multiple Schedules + + Performance engineering is often about exploring different optimization strategies. Different schedules can have dramatically different performance depending on: + - **Problem size**: Small matrices may not benefit from parallelization overhead + - **Hardware**: Cache sizes, vector width, and core count affect optimal tiling + - **Data layout**: Memory access patterns influence cache efficiency + + In this section, we'll write a simple loop to try several schedule configurations and compare their performance. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + `descript` can be used to describe loop structures without specifying every tile size. This allows you to easily write a set of schedules to explore. + + For example: + ```python + i: + j: + k: + j#16: + k#32: + i#i1: unroll=i_r + j#j1: vectorize=j_v + ``` + leaves the sizes of the inner kernel as variables to explore. + + On top of that, variables can also be used to explore configurations for optimizations, for instance: `i#i1: unroll=i_r` allows exploring different unroll sizes, and `j#j1: vectorize=j_v` allows toggling the vectorization. + + Finally, it is also possible to use additional constraints to limit the search space with expert intuitions. For example, limiting the size of the kernel to fit in the CPU registers: `j1+j1*i1 < 128` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **Practice.** The code below defines several schedule configurations using our declarative scheduler. You can try different tile sizes, loop orderings, or optimization combinations. The pre-built search space (`schedule_spec`) may be poorly designed... + """) + return + + +@app.cell +def _(mo): + explore_schedules_intro = mo.ui.code_editor( + value= + '''import xtc.graphs.xtc.op as O + from xtc.search.strategies import Strategy_Descript as Strategy + from xtc.graphs.xtc.graph import XTCGraph + from xtc.backends.tvm import Backend as TVM_Backend + from xtc.backends.mlir import Backend as MLIR_Backend + from xtc.runtimes.host import HostRuntime + from io import StringIO + from contextlib import redirect_stderr + + rt = HostRuntime.get() + + # Problem setup + I, J, K, dtype = 16, 32, 512, "float32" + + def matmul_graph(I: int, J: int, K: int, dtype: str) -> XTCGraph: + """Create a graph computing C = A @ B.""" + a = O.tensor((I, K), dtype, name="A") + b = O.tensor((K, J), dtype, name="B") + with O.graph(name="matmul") as gb: + O.matmul(a, b, name="C") + return gb.graph + + graph = matmul_graph(I=I, J=J, K=K, dtype=dtype) + peak_flops = rt.evaluate_flops(dtype) + + # Evaluation helpers + def apply_schedule(strategy: Strategy, graph, backend_cls, sample): + """Apply a declarative schedule specification and compile.""" + backend = backend_cls(graph) + scheduler = backend.get_scheduler() + strategy.generate(scheduler, sample) + schedule = scheduler.schedule() + comp = backend.get_compiler(dump_file="test_mlir", shared_lib=True) + code = StringIO() + with redirect_stderr(code): + module = comp.compile(schedule) + return module, code.getvalue() + + def evaluate(module, peak_flops, nfmadds): + """Evaluate module performance as percentage of peak.""" + evaluator = module.get_evaluator() + results, _, _ = evaluator.evaluate() + result = min(results) + time_flops = nfmadds / result + perf = time_flops / peak_flops * 100 + return perf + + def acquire(strategy: Strategy, sample: dict, backend): + """Evaluate a single configuration and return its performance.""" + module, _ = apply_schedule(strategy, graph, backend, sample) + return evaluate(module, peak_flops, I*J*K) + + # Run exploration (displays a progress bar and returns sorted results) + def get_info(): + return {"dims": f"{I}x{J}x{K}", "dtype": dtype}''', + language="python", + label="" + ) + + explore_schedules_code = mo.ui.code_editor( + value= + '''def explore(): + """Generator that yields (index, total, name, perf) for each evaluation.""" + schedule_spec = """ + i: + j: + k: + i#i1: unroll=i_u + j#j1: vectorize=j_v + constraints: + - j1+j1*i1<128 + - i1*j1 > 16 + """ + strategy = Strategy(graph, schedule_spec, partial_unrolls=False, partial_tiles=False) + backend = MLIR_Backend + configurations = list(strategy.sample(1000)) + total = len(configurations) + + for idx, sample in enumerate(configurations): + perf = acquire(strategy=strategy, sample=sample, backend=backend) + yield (idx, total, f"{sample}", perf) + + results = run_exploration(explore(), get_info) + ''', + language="python", + label="" + ) + run_explore_button = mo.ui.run_button(label="Run exploration") + mo.vstack([explore_schedules_intro, explore_schedules_code, run_explore_button]) + return explore_schedules_code, explore_schedules_intro, run_explore_button + + +@app.cell +def _( + explore_schedules_code, + explore_schedules_intro, + mo, + plt, + run_exploration, + run_explore_button, + traceback, +): + mo.stop(not run_explore_button.value, mo.md("*Click 'Run exploration' to execute the code.*")) + + # Execute the user's code with run_exploration available + _namespace = {"run_exploration": run_exploration} + try: + exec(explore_schedules_intro.value, _namespace) + exec(explore_schedules_code.value, _namespace) + except Exception: + mo.stop(True, mo.md(f"**Code error:**\n```\n{traceback.format_exc()}\n```")) + + _results, _captured_info, _unsorted_results = _namespace.get("results", ([], {})) + + # Build results summary + if not _results: + mo.stop(True, mo.md("**Error:** No results returned. Make sure to call `results = run_exploration(explore(), get_info)`")) + + _dims = _captured_info.get("dims", "?") + _dtype = _captured_info.get("dtype", "?") + _baseline_perf = _results[-1]["perf"] if _results else 1.0 + _best = _results[0] + + _summary_lines = [ + f"### Schedule Exploration Results", + f"", + f"- **Problem:** {_dims} matmul, {_dtype}", + f"", + f"**Total configurations evaluated:** {len(_results)}", + f"", + f"#### Top 10 configurations:", + f"", + f"| Rank | Configuration | Performance | vs Baseline |", + f"|------|---------------|-------------|-------------|", + ] + + for _rank, _r in enumerate(_results[:10], 1): + _speedup = _r["perf"] / _baseline_perf if _baseline_perf > 0 else 0 + _summary_lines.append(f"| {_rank} | {_r['sample']} | {_r['perf']:.2f}% | {_speedup:.2f}x |") + + _summary_lines.extend([ + f"", + f"#### Best configuration: **{_best['sample']}** ({_best['perf']:.2f}% of peak)", + ]) + + max_perf = _unsorted_results[0]["perf"] + cumul = [max_perf] + for d in _unsorted_results[1:]: + max_perf = max(max_perf, d["perf"]) + if d["perf"] > max_perf: + raise Exception("HEIN") + cumul.append(max_perf) + + + plt.plot(cumul) + plt.xlabel('Number of samples') + plt.ylabel('% of peak performance') + plt.title('Cumulative best performance') + plt.grid(True) + + ax = mo.ui.matplotlib(plt.gca()) + + mo.vstack([mo.md("\n".join(_summary_lines)), ax]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Using descript + + XTC presents two methods to use a `descript` schedule. + + The `descript_scheduler` function allows directly applying a specification to a schedule. Using it also requires specifying the name of the root name, and the axis names used. (See the first example.) + + `Strategy_Descript` allows using a specification with parameters to define a strategy that explores the value space it describes. It works like other XTC strategies, and has two parameters: `partial_unrolls` and `partial_tiles` that, when set to True, allow exploring tile and unroll sizes that do not divide their outer tile. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Transformations + + As shown above, `descript` can be used to explore configurations for optimizations other than loop interchange and tiling. This section will show the supported transformations and the corresponding syntax and exploration parameters. + + These transformations are written on the same line as the tile they apply to. For instance, to unroll a tile of size 16 on the axis `i` by a factor of 4, one would write: ` i#16: unroll=4`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Tiling + + Tile sizes are given with the syntax `axis#size: arguments`. The size can be fixed with an integer, or marked as a parameter to explore with a string. + + By default, tile sizes divide their outer tile sizes. This can be changed by setting `partial_tile=True` in the strategy's arguments (to do so on every tile), or by adding `partial` to the tile's arguments (to only do so on that tile). + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Splitting + + Splitting is given by two possible syntaxes: `axis[start:end]:` and `axis[:size:]:`. + `start`, `end` and `size` can be integers or parameters. `start` and `end` can be ommited on the first/last split of an axis. + + The loops inside of the split need to be tabulated, to indicate where the split ends. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Using splitting and tiling to write a `descript` specification, + ``` + for j in ...: + for i in ...: + for k in ...: + for i1 in range(8): + for j1 in range(8): + for i2 in range(4): + for i3 in range(8, 17): + for j2 in range(8): + for i4 in range(3): + ``` + + Can be described as: + ```python + j: + i: + k: + i[:8]: + j#8: + i#4: + i[8:16]: + j#8: + i#3: + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Unroll, parallelize, and vectorize + + `unroll` by itself mean full loop unrolling. An unroll factor can also be used as seen above. That unroll factor can also be replaced by a variable, which indicates that the unroll factor is a parameter to explore. + + By default, unroll factors divide their respective tile size. This can be changed by setting `partial_unrolls=True` in the strategy's arguments. + + | Unroll syntax | Description | + |-------------------------------------|-------------------------------------------------------------------| + | `unroll` | Full unroll on the corresponding tile | + | `unroll=8` | Unroll of factor 8 on the corresponding tile | + | `unroll=i_unroll` | Unroll factor to be explored (named i_unroll in the constraints) | + + `parallelize` and `vectorize` can only be applied on the outermost (for `parallelize`) or innermost (for `vectorize`) tiles. + + They cannot be up to a factor, but a variable can be used to explore the presence or abscence of the transformation. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Read/Write Buffers + + `descript` also allows specifying read and write buffers, and gives two syntaxes to do so. + + The first syntax follows the XTC API and has two optimizations: `buffer` for write buffers, and `pack` for read buffers. + + `buffer` takes one optional argument: the buffer memory type for the allocation, which can be a string, or `None` to use XTC's default. + + `pack` takes three arguments, in order: the index of the input to pack, the buffer memory type, and wether or not to pad the buffer. + + The second syntax doesn't distiguish between buffer and pack. It is writen like an axis, but using the name of the tensor to buffer instead of an axis: `A: pack`. In the case of packing, `pad` can be added to enable padding. Using this syntax with `descript_scheduler` also requires specifying the same of the matrices (see the example). + + | Buffer syntax | Description | + |-------------------------------------|-------------------------------------------------------------------| + | `buffer` | Write buffer | + | `pack=(0, None, False)` | Read buffer for the first input, with default memory and no padding | + | `B: pack pad` | Read buffer for the second input, with padding | + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Sandbox + + This place is your playground. It is where you will achieve the greatest challenges, + using XTC from scratch ;) + """) + return + + +@app.cell +def _(mo): + sandbox_editor = mo.ui.code_editor( + value= + '''\ + # Implement your challenge here! + print("Hello XTC!")''', + language="python", + label="" + ) + run = mo.ui.run_button(label="Run sandbox") + cancel = mo.ui.button(label="Stop execution", kind="danger") + + mo.vstack( + [ + sandbox_editor, + mo.hstack([run, cancel]), + ] + ) + return cancel, run, sandbox_editor + + +@app.cell +def _(cancel, mo, run, sandbox_editor, start_streaming_execution): + # This output placeholder is what the background thread updates. + out = mo.output + out + + # If cancel is clicked, this cell reruns; that invalidates the previous run-cell, + # making the old background thread's `should_exit` flip to True, and it will terminate. + if cancel.value: + out.replace(mo.md("Cancelled (if something was running, it will stop).")) + else: + mo.stop(not run.value, mo.md("*Click 'Run sandbox' to execute the code, and 'Stop execution' to cancel long runs.*")) + # Start background execution. It will keep streaming until done or cancelled. + start_streaming_execution(code=sandbox_editor.value, out=out, throttle_s=0.05) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py new file mode 100644 index 000000000..a5e18636c --- /dev/null +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -0,0 +1,1085 @@ +import marimo + +__generated_with = "0.19.6" +app = marimo.App(width="full") + +with app.setup: + import os + import sys + import marimo as mo + from sys import platform + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Hardware Performance Counters & Top-down Analysis + + Optimizing code requires understanding exactly how the CPU executes it. + + In this Marimo, we will use **XTC** to compile a Matrix Multiplication and evaluate it using hardware counters and Top-down Microarchitecture Analysis (TMA) + """) + return + + +@app.cell(hide_code=True) +def _(mo, platform): + _warnings = [] + + if platform == "darwin": + _warnings.append(mo.callout(mo.md("**MacOS Detected:** Hardware counters are restricted. The Marimo will run, but TMA metrics might be unavailable."), kind="danger")) + + _warnings.append(mo.callout(mo.md(r""" + **Hardware Counters Prerequisites:** + To allow userspace applications (ring 3) to read CPU PMU counters on Linux, please run the following commands in your terminal: + ```bash + sudo sysctl kernel.perf_event_paranoid=-1 + echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog + ``` + + *Note : Reloading this page may be required* + """), kind="warn")) + + prerequisites_ui = mo.vstack(_warnings) + prerequisites_ui + return prerequisites_ui, + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + + ### For a better experience, it's recommended to be familiar with the XTC's `descript` syntax and the general usage of XTC. + + ```bash + marimo run docs/tutorials/xtc_101.py + marimo run docs/tutorials/descript++_notebook.py + ``` + --- + """) + return + + +@app.cell +def _(supported_arch_msg): + supported_arch_msg + return + + +# µpipe Sankey diagram +@app.cell(hide_code=True) +def _(mo): + def plot_upipe(l1_res, l2_res=None, l3_res=None): + """Generates a hierarchical Sankey diagram for TMA.""" + if not l1_res or len(l1_res) < 4 or l1_res[0] < 0: + return mo.md("⚠️ **TMA Data not available.**") + + try: + import plotly.graph_objects as go + except ImportError: + return mo.callout(mo.md(" **Please install `plotly` (`pip install plotly`) to visualize the Sankey diagram**"), kind="warn") + + try: + # Color palettes (L1, L2, L3) + c_ret = "#4ade80" # Green + c_bad = "#f87171" # Red + c_fe = "#60a5fa" # Blue + c_be = "#c084fc" # Purple + + desc_l1 = { + "Retiring": "Fraction of slots utilized by useful work.", + "Bad Spec": "Fraction of slots wasted due to incorrect speculations.", + "Frontend": "Fraction of slots where the Frontend undersupplies the Backend.", + "Backend": "Fraction of slots where no uops are delivered due to a lack of Backend resources." + } + desc_l2 = { + "Light Ops": "Retiring typical, single-uop instructions.", + "Heavy Ops": "Retiring complex, multi-uop or microcoded instructions.", + "Clears": "Wasted slots due to pipeline flushes.", + "Branch Misp": "Wasted slots due to incorrect branch predictions.", + "Fetch BW": "Frontend cannot decode or deliver enough instructions per cycle.", + "Fetch Lat": "Frontend is completely starved and delivering nothing.", + "Core Bound": "Execution stalled waiting for execution units (ALU/FPU).", + "Mem Bound": "Execution stalled waiting for data from the memory (L1/L2/L3/RAM)." + } + desc_l3 = [ + "Stalls due to branch resteers.", + "Cycles where Divider unit was active.", + "Stalled on external memory (DRAM).", + "Limited by Decoded Stream Buffer pipeline.", + "Stalls due to switching from DSB to MITE.", + "Instructions decoded into 2 or more uops.", + "Floating-point (FP) operations executed.", + "One uop representing multiple contiguous instructions.", + "Stalls due to instruction cache misses.", + "Stalls due to ITLB misses.", + "Stalled without missing the L1D cache.", + "Stalled due to L2 cache accesses.", + "Stalled due to L3 cache accesses or Core contention.", + "Stalls due to Length Changing Prefixes.", + "Memory load or store uops retired.", + "Uops fetched by the Microcode Sequencer.", + "Limited by MITE pipeline (legacy decode).", + "Stalls due to switching uop delivery to MS unit.", + "Branch instructions that were not fused.", + "Remaining light uops not covered by sibling nodes.", + "Stalls due to other misprediction cases.", + "Machine Clears not related to memory ordering.", + "Stalled on accesses to Persistent Memory Modules.", + "Limited due to execution ports saturation.", + "Issue-pipeline stalled due to serializing operations.", + "Stalled due to store memory accesses and RFO requests." + ] + + nodes_label = [] + nodes_color = [] + nodes_desc = [] + links_source = [] + links_target = [] + links_value = [] + links_color = [] + + def hex_to_rgba(hex_color, alpha=0.4): + h = hex_color.lstrip('#') + return f"rgba({int(h[0:2], 16)}, {int(h[2:4], 16)}, {int(h[4:6], 16)}, {alpha})" + + def add_node(name, pct, color, desc=""): + nodes_label.append(f"{name} ({pct:.1f}%)") + nodes_color.append(color) + nodes_desc.append(desc) + return len(nodes_label) - 1 + + def add_link(source, target, value, color): + links_source.append(source) + links_target.append(target) + links_value.append(max(value, 0.1)) # 0.1 ensures 0% metrics remain visible as thin lines + links_color.append(hex_to_rgba(color, 0.4)) + + # TopdownL1 + n_ret = add_node("Retiring", l1_res[0], c_ret, desc_l1["Retiring"]) + n_bad = add_node("Bad Spec", l1_res[1], c_bad, desc_l1["Bad Spec"]) + n_fe = add_node("Frontend", l1_res[2], c_fe, desc_l1["Frontend"]) + n_be = add_node("Backend", l1_res[3], c_be, desc_l1["Backend"]) + + # TopdownL2, TopdownL3 + if l2_res and len(l2_res) >= 8 and l2_res[0] >= 0: + def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): + l2_val = l2_res[l2_idx] + n_l2 = add_node(l2_name, l2_val, color, desc) + add_link(parent_node, n_l2, l2_val, color) + + if l3_res and l3_mappings and len(l3_res) > 0 and l3_res[0] >= 0: + for l3_name, l3_idx in l3_mappings: + if l3_idx < len(l3_res): + l3_val = l3_res[l3_idx] + desc_text = desc_l3[l3_idx] if len(desc_l3) > l3_idx else "" + n_l3 = add_node(l3_name, l3_val, color, desc_text) + add_link(n_l2, n_l3, l3_val, color) + + map_light = map_heavy = map_clears = map_misp = map_fbw = map_flat = map_core = map_mem = None + + if l3_res and len(l3_res) >= 26: + map_light = [("FP Arith", 6), ("Mem Ops", 14), ("Fused", 7), ("Non-Fused", 18), ("Other Light", 19)] + map_heavy = [("Few Uops", 5), ("Microcode", 15)] + map_clears = [("Other Nukes", 21)] + map_misp = [("Branch Rest.", 0), ("Other Misp.", 20)] + map_fbw = [("MITE", 16), ("DSB", 3), ("DSB Swit.", 4), ("LCP", 13)] + map_flat = [("ICache Miss", 8), ("ITLB Miss", 9), ("MS Switches", 17)] + map_core = [("Divider", 1), ("Ports Util.", 23), ("Serializing", 24)] + map_mem = [("L1 Bound", 10), ("L2 Bound", 11), ("L3 Bound", 12), ("DRAM Bound", 2), ("Store Bound", 25), ("PMM Bound", 22)] + elif l3_res and len(l3_res) >= 5: # Support for TopdownL3_Mem + map_mem = [("L1 Bound", 0), ("L2 Bound", 1), ("L3 Bound", 2), ("DRAM Bound", 3), ("Store Bound", 4)] + + process_l2("Light Ops", 0, n_ret, c_ret, desc_l2["Light Ops"], map_light) + process_l2("Heavy Ops", 1, n_ret, c_ret, desc_l2["Heavy Ops"], map_heavy) + process_l2("Clears", 2, n_bad, c_bad, desc_l2["Clears"], map_clears) + process_l2("Branch Misp", 3, n_bad, c_bad, desc_l2["Branch Misp"], map_misp) + process_l2("Fetch BW", 4, n_fe, c_fe, desc_l2["Fetch BW"], map_fbw) + process_l2("Fetch Lat", 5, n_fe, c_fe, desc_l2["Fetch Lat"], map_flat) + process_l2("Core Bound", 6, n_be, c_be, desc_l2["Core Bound"], map_core) + process_l2("Mem Bound", 7, n_be, c_be, desc_l2["Mem Bound"], map_mem) + + fig = go.Figure(data=[go.Sankey( + node = dict( + pad = 15, + thickness = 20, + line = dict(color = "black", width = 0.5), + label = nodes_label, + color = nodes_color, + customdata = nodes_desc, + hovertemplate = "%{label}
%{customdata}" + ), + link = dict( + source = links_source, + target = links_target, + value = links_value, + color = links_color, + hovertemplate = "%{source.label} → %{target.label}
Value: %{value:.1f}%" + ) + )]) + + fig.update_layout( + title_text="Top-down Microarchitecture Pipeline µpipe", + font_size=12, + height=650 if (l3_res and len(l3_res) > 5) else 450, + margin=dict(t=40, l=20, r=20, b=20) + ) + + return fig + + except Exception as e: + import traceback + error_trace = traceback.format_exc() + return mo.md(f"**Error generating Sankey diagram:**\n```python\n{error_trace}\n```") + + return plot_upipe, + + + + +@app.cell(hide_code=True) +def _(mo): + btn_all = mo.ui.run_button(label="Run All Experiments", kind="success") + + run_all_ui = mo.vstack([ + mo.md("---"), + mo.md("## Evaluate All Steps"), + mo.md("Click the button below to compile and evaluate all steps at once."), + btn_all + ]) + + run_all_ui + return btn_all, run_all_ui + + + +@app.cell(hide_code=True) +def _(mo, platform, plot_upipe): + def run_experiment(schedule_spec): + import xtc.graphs.xtc.op as O + from xtc.backends.mlir import Backend + from xtc.schedules.descript import descript_scheduler + from xtc.runtimes.host import HostRuntime + import subprocess + + # Fixed matrix size for all tests + I, J, K, dtype = 512, 512, 512, "float32" + + a = O.tensor((I, K), dtype, name="A") + b = O.tensor((K, J), dtype, name="B") + with O.graph(name="matmul") as gb: + O.matmul(a, b, name="C") + + backend = Backend(gb.graph) + scheduler = backend.get_scheduler() + + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + abstract_matrix=["A", "B", "C"], + spec=schedule_spec + ) + + compiler = backend.get_compiler( + dump_file="matmul_mlir", + shared_lib=True, + print_assembly=True + ) + module = compiler.compile(scheduler.schedule()) + + rt = HostRuntime.get() + peak_flops = rt.evaluate_flops(dtype) + + # Evaluate pure execution time + eval_time = module.get_evaluator(validate=False) + time_results, _, _ = eval_time.evaluate() + exec_time = min(time_results) + + # Calculate percentage of theoretical peak (MACs / Time / Peak_MACs) + peak_perf_pct = (I * J * K) / exec_time / peak_flops * 100 + + # Visual progress bar for Peak Perf + bar_color = "#ef4444" if peak_perf_pct < 10 else ("#f59e0b" if peak_perf_pct < 50 else "#22c55e") + fill_width = max(min(peak_perf_pct, 100), 2) # Ensure at least 2% so the text is visible + + perf_ui = mo.Html(f''' +
+ Computational Efficiency (Theoretical Peak): +
+
+ {peak_perf_pct:.1f}% +
+
+
+ ''') + + + + tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] + evaluator = module.get_evaluator(validate=False, hw_counters=tma_counters) + res, code, err = evaluator.evaluate() + + + if err: return mo.md(f"**Error:**\n```text\n{err}\n```") + + l1_res = res[0:4] if len(res) >= 4 else None + l2_res = res[4:12] if len(res) >= 12 else None + l3_res = res[12:38] if len(res) >= 38 else None + + + so_path = getattr(module, "file_name", None) + if so_path: + try: + # Silently dump the assembly to a Python string + asm_code = subprocess.check_output(["objdump", "--disassemble=matmul", so_path], universal_newlines=True) + except Exception as e: + asm_code = f"Failed to extract assembly: {e}" + + return mo.vstack([ + perf_ui, + plot_upipe(l1_res, l2_res, l3_res), + + mo.accordion({ + "View XTC Schedule Source": mo.md(f"```yaml\n{schedule_spec}\n```"), + "View Generated Assembly": mo.md(f"```x86asm\n{asm_code}\n```") + }) + + ]) + return run_experiment, + + +@app.cell(hide_code=True) +def _(mo): + btn_ex1 = mo.ui.run_button(label="Run Step 1", kind="neutral") + + mo.md(f""" + --- + ## Step 1: Naive Implementation + A standard, unoptimized Matrix Multiplication. The CPU executes instructions sequentially. + + ```python + for i in 512: + for j in 512: + for k in 512: + C[i, j] += A[i, k] * B[k, j] + ``` + *Expectation: High Backend Bound (Memory latency).* + + {btn_ex1} + """) + return btn_ex1, + + +@app.cell(hide_code=True) +def _(btn_ex1, btn_all, mo, run_experiment): + mo.stop(not (btn_ex1.value or btn_all.value)) + + spec_1 = ''' +i: +j: +k: +''' + run_experiment(spec_1) + return spec_1, + + +@app.cell(hide_code=True) +def _(mo): + btn_ex2 = mo.ui.run_button(label="Run Step 2", kind="neutral") + + mo.md(f""" + --- + ## Step 2: Vectorization + Processing multiple elements per instruction using SIMD (Single Instruction, Multiple Data). + The inner loop disappears completely: the CPU computes 16 elements at once in a single clock cycle. + + ```python + for i in range(512): + for j in range(0, 512, 16): # Steps of 16 + for k in range(512): + + # Vectorized execution (e.g., AVX-512) + # A single SIMD instruction computes 16 elements + C[i, j : j+16] += A[i, k] * B[k, j : j+16] + ``` + + *Expectation: Higher Retiring, but still heavily bottlenecked by memory loads since `C` is read and written 512 times for nothing.* + + {btn_ex2} + """) + return btn_ex2, + + +@app.cell(hide_code=True) +def _(btn_ex2, btn_all, mo, run_experiment): + mo.stop(not (btn_ex2.value or btn_all.value)) + + spec_2 = ''' +i: +j: +k: +j#16: vectorize +''' + run_experiment(spec_2) + return spec_2, + + +@app.cell(hide_code=True) +def _(mo): + btn_ex3 = mo.ui.run_button(label="Run Step 3", kind="neutral") + + mo.md(f""" + --- + ## Step 3: Register Tiling (2x16) + Unrolling the outer loop allows the CPU to process multiple vectors simultaneously. The compiler duplicates the instructions, breaking data dependency chains and keeping accumulators inside physical registers. + + ```python + for i in range(0, 512, 2): # Steps of 2 + for j in range(0, 512, 16): # Steps of 16 + for k in range(512): + + # Unrolled & Vectorized (2x16) + # Two independent FMAs are exposed to the CPU per k-iteration + C[i, j : j+16] += A[i, k] * B[k, j : j+16] + C[i + 1, j : j+16] += A[i + 1, k] * B[k, j : j+16] + ``` + + *Expectation: Retiring improves (Instruction-Level Parallelism), but the CPU still re-loads the accumulator `C` at every `k` iteration.* + + {btn_ex3} + """) + return btn_ex3, + + +@app.cell(hide_code=True) +def _(btn_ex3, btn_all, mo, run_experiment): + mo.stop(not (btn_ex3.value or btn_all.value)) + + spec_3 = ''' +i: +j: +k: +i#2: unroll +j#16: vectorize +''' + run_experiment(spec_3) + return spec_3, + + +@app.cell(hide_code=True) +def _(mo): + btn_ex4 = mo.ui.run_button(label="Run Step 4", kind="neutral") + + mo.md(f""" + --- + ## Step 4: Load/Store Hoisting + By interchanging the `k` loop *outside* of the unrolled micro-kernel, we prevent the compiler from loading and storing the `C` matrix to L1 cache repeatedly. `C` stays in the vector registers during the entire `k` accumulation. + + ```python + I = 512 + J = 512 + K = 512 + # Global navigation (RAM) + for i_out in range(I / 2): # Steps of 2 + for j_out in range(J / 16): # Steps of 16 + + # Load hosting + C_vec_0 = C[i, j : j+16] + C_vec_1 = C[i + 1, j : j+16] + + # Accumulation loop + for k in range(K) + + # Vector Load (16 floats loaded at once) + B_vec = B[k, j : j+16] + + # Unrolled & Vectorized Micro-kernel + C_vec_0 += A[i, k] * B_vec + C_vec_1 += A[i + 1, k] * B_vec + + # Store hoisting + C[i, j : j+16] = C_vec_0 + C[i + 1, j : j+16] = C_vec_1 + ``` + + *Expectation: Massive drop in L1 Bound since C is no longer repeatedly spilled to the cache.* + + {btn_ex4} + """) + return btn_ex4, + + +@app.cell(hide_code=True) +def _(btn_ex4, btn_all, mo, run_experiment): + mo.stop(not (btn_ex4.value or btn_all.value)) + + spec_4 = ''' + i: + j: + i#2: unroll + j#16: vectorize + k: vectorize + ''' + run_experiment(spec_4) + return spec_4, + + +@app.cell(hide_code=True) +def _(mo): + btn_ex5 = mo.ui.run_button(label="Run Step 5", kind="neutral") + + mo.md(f""" + --- + ## Step 5: Cache Tiling + + Large matrices evict each other from L1/L2 caches. We tile the problem into 64x64 blocks to ensure data perfectly fits in the fast cache hierarchy. + + + ```python + # Global navigation (RAM / L3 Cache) + I = 512 + J = 512 + K = 512 + for i_out in range(I / 64): + for j_out in range(J / 64): + for k_out in range(K / 64): + + # Block navigation (L2 / L1 Cache) + for i_in in range(64 / 2): # Steps of 2 + for j_in in range(64 / 16): # Steps of 16 + + # Load hoisting + C_reg_0 = C[i, j : j+16] + C_reg_1 = C[i + 1, j : j+16] + + # Accumulation loop + for k_in in range(64): + B_vec = B[k, j : j+16] # Broadcasted or Vector loaded + + # Unrolled micro-kernel + C_reg_0 += A[i, k] * B_vec + C_reg_1 += A[i + 1, k] * B_vec + + # Store hoisting + # C[i_out...i_in, j_out...j_in] = C_reg + C[i, j : j+16] = C_reg_0 + C[i + 1, j : j+16] = C_reg_1 + ``` + + *Expectation: Memory Bound drops significantly. Computation is now purely limited by execution units.* + + {btn_ex5} + """) + return btn_ex5, + + +@app.cell(hide_code=True) +def _(btn_ex5, btn_all, mo, run_experiment): + mo.stop(not (btn_ex5.value or btn_all.value)) + + spec_5 = ''' + i: + j: + k: + i#64: + j#64: + k#64: + i#2: unroll + j#16: vectorize + ''' + run_experiment(spec_5) + return spec_5, + + +@app.cell(hide_code=True) +def _(mo): + btn_ex6 = mo.ui.run_button(label="Run Step 6", kind="neutral") + + mo.md(f""" + --- + ## Step 6: Peak Optimization (3x16) + We maximize physical register usage. A 3x16 tile uses 3 AVX-512 registers for `C`, maximizing Instruction-Level Parallelism without spilling to the L1 cache. + + ```python + # Global navigation (RAM / L3 Cache) + for i_out in range(I / 64): + for j_out in range(J / 64): + for k_out in range(K / 64): + + # Block navigation (L2 / L1 Cache) + for i_in in range(64 / 3): # Steps of 3 + for j_in in range(64 / 16): # Steps of 16 + + # Load hoisting + C_reg_0 = C[i, j : j+16] + C_reg_1 = C[i + 1, j : j+16] + C_reg_2 = C[i + 2, j : j+16] + + # Accumulation loop + for k_in in range(64): + B_vec = B[k, j : j+16] # Broadcasted or Vector loaded + + # Micro-kernel (Physical Registers) + #for i_unroll in range(3): # Unrolled 3x + # for j_vec in range(16): # Vectorized 16x + # C_reg[...] += A[...] * B[...] + + # Unrolled micro-kernel + C_reg_0 += A[i, k] * B_vec + C_reg_1 += A[i + 1, k] * B_vec + C_reg_2 += A[i + 2, k] * B_vec + + # Store hoisting + # C[i_out...i_in, j_out...j_in] = C_reg + C[i, j : j+16] = C_reg_0 + C[i + 1, j : j+16] = C_reg_1 + C[i + 2, j : j+16] = C_reg_2 + ``` + + {btn_ex6} + """) + return btn_ex6, + + +@app.cell(hide_code=True) +def _(btn_ex6, btn_all, mo, run_experiment): + mo.stop(not (btn_ex6.value or btn_all.value)) + + spec_6 = ''' + i: + j: + k: + i#64: + j#64: + k#64: + i#3: unroll + j#16: vectorize + ''' + run_experiment(spec_6) + return spec_6, + + + +@app.cell(hide_code=True) +def _(): + btn_ex7 = mo.ui.run_button(label="Run Step 7", kind="neutral") + + mo.md(f""" + --- + ## Step 7: Data Packing (Memory Layout Optimization) + Even with cache tiling, reading original matrices `A` and `B` with large memory strides causes cache line fragmentation and TLB (Translation Lookaside Buffer) misses. + By **packing** the data, the compiler copies the required blocks into contiguous temporary buffers before the inner loops, ensuring perfect linear memory access for the micro-kernel. + + ```python + # Global navigation (RAM / L3 Cache) + for i_out in range(I / 64): + for j_out in range(J / 64): + for k_out in range(K / 64): + + # Data packing + # Copy non-contiguous tiles into contiguous L2/L1 buffers + A_pack = contiguous_copy(A[i_out*64 : i_out*64+64, k_out*64 : k_out*64+64]) + B_pack = contiguous_copy(B[k_out*64 : k_out*64+64, j_out*64 : j_out*64+64]) + + # Block navigation (L2 / L1 Cache) + for i_in in range(64 / 3): + for j_in in range(64 / 16): + + # Load hoisting + C_vec_0 = C[i, j : j+16] + C_vec_1 = C[i + 1, j : j+16] + C_vec_2 = C[i + 2, j : j+16] + + # Accumulation loop + for k_in in range(64): + # Reading from contiguous packed buffer + B_vec = B_pack[k_in, j_in : j_in+16] + + # Unrolled Micro-kernel + C_vec_0 += A_pack[i_in, k_in] * B_vec + C_vec_1 += A_pack[i_in + 1, k_in] * B_vec + C_vec_2 += A_pack[i_in + 2, k_in] * B_vec + + # Store hoisting + C[i, j : j+16] = C_vec_0 + C[i + 1, j : j+16] = C_vec_1 + C[i + 2, j : j+16] = C_vec_2 + ``` + *Expectation: TLB misses drop significantly. Memory Bound achieves its lowest possible value.* + + {btn_ex7} + """) + return + + +@app.cell(hide_code=True) +def _(btn_ex7, btn_all, mo, run_experiment): + mo.stop(not (btn_ex7.value or btn_all.value)) + + spec_7 = ''' + i: + j: + k: + A: pack + B: pack + i#64: + j#64: + k#64: + i#3: unroll + j#16: vectorize + ''' + run_experiment(spec_7) + return + + + +@app.cell(hide_code=True) +def _(): + _sandbox_code = """import xtc.graphs.xtc.op as O + from xtc.backends.mlir import Backend + from xtc.schedules.descript import descript_scheduler + + # 1. Problem setup + I, J, K, dtype = 256, 256, 256, "float32" + a = O.tensor((I, K), dtype, name="A") + b = O.tensor((K, J), dtype, name="B") + + with O.graph(name="matmul") as gb: + O.matmul(a, b, name="C") + + # 2. Scheduling + backend = Backend(gb.graph) + scheduler = backend.get_scheduler() + + schedule_spec = ''' + i: + j: + k: + A: pack + B: pack + i#64: + j#64: + k#64: + i#3: unroll + j#16: vectorize + ''' + + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + abstract_matrix=["A", "B", "C"], + spec=schedule_spec + ) + sched = scheduler.schedule() + + # 3. Compilation + compiler = backend.get_compiler( + dump_file="sandbox_mlir", + shared_lib=True, + print_assembly=False + ) + module = compiler.compile(sched) + + # 4. Evaluation + import sys + hw_counters = ["Cycles","TopdownL1", "TopdownL2","TopdownL3_Mem"] if sys.platform == "linux" else [] + evaluator = module.get_evaluator(validate=True, hw_counters=hw_counters) + + print("Evaluating Schedule...") + results, code, err = evaluator.evaluate() + + if err: + print(f"Error:\\n{err}") + else: + print(f"Raw Results Array: {[round(x, 2) for x in results]}") + """ + api_sandbox_editor = mo.ui.code_editor( + value=_sandbox_code, + language="python", + label="XTC Full API Sandbox" + ) + btn_api_sandbox = mo.ui.run_button(kind="neutral", label="Run Sandbox") + + sandbox_ui = mo.vstack([ + mo.md(r""" + --- + ## Complete API Sandbox + + Below is a fully editable XTC script. It demonstrates the complete API workflow: defining the computational graph, applying a schedule, compiling to MLIR, and running the hardware evaluation. + + Feel free to modify the tensor sizes, the schedule specification, or the list of hardware counters. If `Topdown` metrics are requested, the microarchitecture pipeline diagram will automatically be rendered below the output. + """), + api_sandbox_editor, + btn_api_sandbox + ]) + return api_sandbox_editor, btn_api_sandbox, sandbox_ui + + +@app.cell +def _(sandbox_ui): + sandbox_ui + return + + +@app.cell +def _(mo): + tma_support_content = mo.md( + """ + | Microarchitecture | Supported TMA Levels | Execution Mode | + |---|---|---| + | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem`, `TopdownL3` | Native API | + | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem`, `TopdownL3` | Native API | + | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API | + | **ARM** | `TopdownL1 ?`, `TopdownL2 ?` | Native API (untested) | + | **Generic Linux (`perf` tool)** | `TopdownL1` to `TopdownL6` | System Fallback *(Multiplexed)* | + + *Note 1: TopdownL3_Mem returns [L1 Bound, L2 Bound, L3 Bound, DRAM Bound, Store Bound].* + + *Note 2: AMD architectures do not have native Topdown metrics. They are rebuilt using AMD-specific hardware events and pipeline width formulas.* + """ + ) + + amd_zen4_formulas = mo.md( + """ + **Zen 4 Pipeline Width:** 6 slots per cycle. + `Total Slots = Cycles * 6` + + **Level 1 Formulas:** + * **Frontend Bound:** `Frontend Stalls / Total Slots` + * **Retiring:** `Ops Retired / Total Slots` + * **Bad Speculation:** `(Ops Dispatched - Ops Retired) / Total Slots` + * **Backend Bound:** `100% - (Frontend Bound + Retiring + Bad Speculation)` + + **Level 2 Formulas:** + * **Fetch Latency:** `Fetch Latency Stalls / Total Slots` + * **Fetch Bandwidth:** `Frontend Bound - Fetch Latency` + * **Memory Bound:** `Backend Memory Stalls / Total Slots` + * **Core Bound:** `Backend CPU Stalls / Total Slots` + * **Branch Mispredicts:** `Branch Mispredict Stalls / Total Slots` + * **Machine Clears:** `Pipeline Restarts / Total Slots` + * **Heavy Ops:** `Microcode Ops Retired / Total Slots` + * **Light Ops:** `Total Retiring - Heavy Ops` + """ + ) + + zen4_accordion = mo.accordion({ + "Show AMD Zen 4 TMA reconstruction formulas": amd_zen4_formulas + }) + + fallback_note = mo.md("*Unsupported metrics will automatically use the `perf` fallback.*") + + supported_arch_msg = mo.accordion({ + "View supported TMA architectures": mo.vstack([ + tma_support_content, + zen4_accordion, + fallback_note + ]) + }) + + return (supported_arch_msg,) + +@app.cell +def _(mo): + l1_md = mo.md( + """ + **Array Order:** `[Retiring, Bad Speculation, Frontend Bound, Backend Bound]` + + | Index | Metric | Description | + |---|---|---| + | `[0]` | **Retiring** | Fraction of slots utilized by useful work (issued uops that get retired). | + | `[1]` | **Bad Speculation** | Fraction of slots wasted due to incorrect speculations. | + | `[2]` | **Frontend Bound** | Fraction of slots where the Frontend undersupplies the Backend. | + | `[3]` | **Backend Bound** | Fraction of slots where no uops are delivered due to a lack of Backend resources. | + """ + ) + + l2_md = mo.md( + """ + **Array Order:** `[Light Ops, Heavy Ops, Machine Clears, Branch Mispredicts, Fetch Bandwidth, Fetch Latency, Core Bound, Memory Bound]` + + | Index | Category | Sub-Metric | Description | + |---|---|---|---| + | `[0]` | `Retiring` | `light_operations` | Retiring typical, single-uop instructions. | + | `[1]` | `Retiring` | `heavy_operations` | Retiring complex, multi-uop or microcoded instructions. | + | `[2]` | `Bad Speculation` | `machine_clears` | Wasted slots due to pipeline flushes (e.g., memory ordering issues, exceptions). | + | `[3]` | `Bad Speculation` | `branch_mispredicts` | Wasted slots due to incorrect branch predictions. | + | `[4]` | `Frontend Bound` | `fetch_bandwidth` | Frontend cannot decode or deliver enough instructions per cycle. | + | `[5]` | `Frontend Bound` | `fetch_latency` | Frontend is completely starved and delivering nothing (e.g., Instruction Cache miss). | + | `[6]` | `Backend Bound` | `core_bound` | Execution stalled waiting for execution units (ALU/FPU) or due to data dependencies. | + | `[7]` | `Backend Bound` | `memory_bound` | Execution stalled waiting for data from the memory (L1/L2/L3/RAM). | + """ + ) + + l3_md = mo.md( + """ + | Index | Category | Metric | Description | + |---|---|---|---| + | `[0]` | `Bad Speculation` | `branch_resteers` | Stalls due to branch resteers at execution stage. | + | `[1]` | `Backend Bound` | `divider` | Cycles where the Divider unit was active. | + | `[2]` | `Backend Bound` | `dram_bound` | Stalled on external memory (DRAM) accesses. | + | `[3]` | `Frontend Bound` | `dsb` | Limited by Decoded Stream Buffer (uop cache) pipeline. | + | `[4]` | `Frontend Bound` | `dsb_switches` | Stalls due to switching from DSB to MITE pipelines. | + | `[5]` | `Retiring` | `few_uops_instructions` | Instructions decoded into 2 or more uops. | + | `[6]` | `Retiring` | `fp_arith` | Floating-point (FP) operations executed. | + | `[7]` | `Retiring` | `fused_instructions` | One uop representing multiple contiguous instructions. | + | `[8]` | `Frontend Bound` | `icache_misses` | Stalls due to instruction cache misses. | + | `[9]` | `Frontend Bound` | `itlb_misses` | Stalls due to Instruction TLB (ITLB) misses. | + | `[10]` | `Backend Bound` | `l1_bound` | Stalled without missing the L1 Data (L1D) cache. | + | `[11]` | `Backend Bound` | `l2_bound` | Stalled due to L2 cache accesses. | + | `[12]` | `Backend Bound` | `l3_bound` | Stalled due to L3 cache accesses or sibling Core contention. | + | `[13]` | `Frontend Bound` | `lcp` | Stalls due to Length Changing Prefixes. | + | `[14]` | `Retiring` | `memory_operations` | Memory load or store uops retired. | + | `[15]` | `Retiring` | `microcode_sequencer` | Uops fetched by the Microcode Sequencer (MS) unit. | + | `[16]` | `Frontend Bound` | `mite` | Limited by MITE pipeline (legacy decode pipeline). | + | `[17]` | `Frontend Bound` | `ms_switches` | Stalls due to switching uop delivery to the MS unit. | + | `[18]` | `Retiring` | `non_fused_branches` | Branch instructions that were not fused. | + | `[19]` | `Retiring` | `other_light_ops` | Remaining light uops not covered by other sibling nodes. | + | `[20]` | `Bad Speculation` | `other_mispredicts` | Stalls due to other misprediction cases (non-retired branches). | + | `[21]` | `Bad Speculation` | `other_nukes` | Machine Clears not related to memory ordering. | + | `[22]` | `Backend Bound` | `pmm_bound` | Stalled on accesses to Persistent Memory Modules (Optane). | + | `[23]` | `Backend Bound` | `ports_utilization` | Limited due to execution ports saturation (FPU/ALU contention). | + | `[24]` | `Backend Bound` | `serializing_operation` | Issue-pipeline stalled due to serializing operations. | + | `[25]` | `Backend Bound` | `store_bound` | Stalled due to store memory accesses and Read For Ownership(RFO) requests. | + """ + ) + + l4_md = mo.md( + """ + + | Metric | Description | + |---|---| + | `4k_aliasing` | Load accesses aliased by preceding stores with a 4K address offset. | + | `assists` | Uops delivered by Microcode Sequencer as a result of Assists. | + | `cisc` | Uops originated from CISC instructions. | + | `clears_resteers` | Branch Resteers as a result of Machine Clears. | + | `code_stlb_hit` / `miss` | ITLB missed, hitting or missing Second-level TLB (STLB). | + | `contested_accesses` | Memory handling synchronizations due to contested accesses. | + | `data_sharing` | Memory handling synchronizations due to data-sharing. | + | `decoder0_alone` | Decoder-0 was the only active decoder. | + | `dtlb_load` / `store` | DTLB missed by load or store accesses. | + | `false_sharing` | CPU handling synchronizations due to False Sharing. | + | `fb_full` | L1D Fill Buffer unavailability limited memory accesses. | + | `fp_scalar` / `vector` | Arithmetic FP scalar or vector uops retired. | + | `l1_latency_dependency` | Demand load accesses that hit the L1D cache. | + | `l2_hit_latency` / `l3` | Demand load accesses that hit L2 or L3 under unloaded scenarios. | + | `lock_latency` | Cache misses due to lock operations. | + | `mem_bandwidth` | Approaching bandwidth limits of external memory (DRAM/HBM). | + | `mem_latency` | Latency from external memory (DRAM/HBM). | + | `mispredicts_resteers` | Branch Resteers due to Branch Misprediction. | + | `nop_instructions` | NOP (no op) instructions retired. | + | `ports_utilized_0/1/2/3m` | CPU executed 0, 1, 2, or 3+ uops per cycle on all execution ports. | + | `split_loads` / `stores` | Handling memory split accesses crossing 64-byte boundaries. | + | `sq_full` | Super Queue (SQ) was full. | + | `store_fwd_blk` | Loads blocked unable to forward data from earlier overlapping stores. | + | `store_latency` | CPU handling L1D store misses. | + | `unknown_branches` | Stalls due to new branch address clears. | + | `x87_use` | Approximation of legacy x87 usage. | + """ + ) + + l5_md = mo.md( + """ + + | Metric | Description | + |---|---| + | `alu/load/store_op_utilization` | Cycles CPU dispatched uops on execution ports for ALU, Load, or Store. | + | `fp_assists` | Uops retired as a result of handing Floating Point (FP) Assists. | + | `fp_vector_128b/256b/512b` | FP vector uops retired for 128, 256, or 512-bit wide vectors. | + | `load/store_stlb_hit/miss` | DTLB/TLB missed by load/store accesses, hitting or missing STLB. | + | `local/remote_mem` | Memory access constrained by local or remote NUMA nodes. | + | `mixing_vectors` | Penalty for injected blend uops. | + """ + ) + + l6_md = mo.md( + """ + + | Metric | Description | + |---|---| + | `port_0` to `port_7` | Fraction of cycles the CPU dispatched uops on specific hardware execution ports (e.g., ALU, Loads, Stores). | + | `load/store_stlb_miss_X` | Cycles to walk memory paging structures for 4K, 2M or 1G pages. | + """ + ) + + reminder_output_msg = mo.vstack([ + mo.md("**TMA Metrics Dictionary:**"), + mo.accordion({ + "Level 1 (4 metrics)": l1_md, + "Level 2 (8 metrics)": l2_md, + "Level 3 (26 metrics)": l3_md, + "Level 4 (32 metrics)": l4_md, + "Level 5 (15 metrics)": l5_md, + "Level 6 (8 metrics)": l6_md + }) + ]) + + return reminder_output_msg, + +@app.cell +def _(reminder_output_msg): + reminder_output_msg + return + + +@app.cell +def _(api_sandbox_editor, btn_api_sandbox): + mo.stop(not btn_api_sandbox.value, mo.md("*Click 'Run Sandbox' to see the output.*")) + + import io + from contextlib import redirect_stdout, redirect_stderr + import traceback + + out = io.StringIO() + err = io.StringIO() + local_vars = {} + + with redirect_stdout(out), redirect_stderr(err): + try: + exec(api_sandbox_editor.value, globals().copy(), local_vars) + except Exception as e: + traceback.print_exc() + + stdout_val = out.getvalue() + stderr_val = err.getvalue() + + output_blocks = [] + if stdout_val: + output_blocks.append(mo.md(f"**Standard Output:**\n```text\n{stdout_val}\n```")) + if stderr_val: + output_blocks.append(mo.md(f"**Standard Error / Exceptions:**\n```text\n{stderr_val}\n```")) + + if not stdout_val and not stderr_val: + output_blocks.append(mo.md("*Script executed successfully with no output.*")) + + hw_counters = local_vars.get("hw_counters", []) + results = local_vars.get("results", []) + code = local_vars.get("code", -1) + + if code == 0 and hw_counters and results: + sizes = {"TopdownL1": 4, "TopdownL2": 8, "TopdownL3": 26, "TopdownL3_Mem": 5} + l1_res, l2_res, l3_res = None, None, None + curr_idx = 0 + + for c in hw_counters: + s = sizes.get(c, 1) + chunk = results[curr_idx : curr_idx + s] + + if c == "TopdownL1": l1_res = chunk + if c == "TopdownL2": l2_res = chunk + if c in ["TopdownL3", "TopdownL3_Mem"]: l3_res = chunk + + curr_idx += s + + if l1_res and len(l1_res) >= 4 and l1_res[0] >= 0: + output_blocks.append(mo.md("**Generated Top-down Diagram:**")) + output_blocks.append(plot_upipe(l1_res, l2_res, l3_res)) + + mo.vstack(output_blocks), + return mo.vstack(output_blocks), + + +if __name__ == "__main__": + app.run() diff --git a/pyproject.toml b/pyproject.toml index c74c01364..53a2b40e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,10 @@ dev = [ "pytest-xdist", "pyright==1.1.407", "types-PyYAML", - "marimo==0.19.6", + "marimo>=0.20.0", "ruff==0.14.10", + "matplotlib", + "plotly" ] [tool.setuptools] @@ -66,7 +68,7 @@ reportMissingImports = false #reportMissingTypeArgument = "error" [tool.mypy] -python_version = "3.10" +python_version = "3.12" packages = ["xtc", "docs.marimo"] ignore_missing_imports = true check_untyped_defs = true @@ -102,3 +104,6 @@ exclude = [ ] line-length = 88 indent-width = 4 + +[tool.marimo.runtime] +output_max_bytes = 11_000_000 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index a33b70787..9a085a18e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -11,3 +11,5 @@ pytest-xdist pyright==1.1.407 types-PyYAML ruff==0.14.10 +altair +plotly diff --git a/src/xtc/cli/display_results.py b/src/xtc/cli/display_results.py index 16bf1cf5e..d8a8d045b 100644 --- a/src/xtc/cli/display_results.py +++ b/src/xtc/cli/display_results.py @@ -85,16 +85,27 @@ def draw_cor( Y: Sequence[float], ref_label: str | None = None, label: str | None = None, + alpha: float = 1, ): ax.scatter( Yref, Y, label=label, + alpha=alpha, ) if ref_label: ax.set_xlabel(ref_label) +def draw_diag( + ax: Any, + ymin: float = 0, + ymax: float = 1, +): + xy = [[ymin, ymax]] * 2 + ax.plot(*xy, alpha=0.6, color="grey", linestyle="--") + + def save_fig(fname: str | Path): fig = plt.gcf() dpi = fig.dpi @@ -120,6 +131,11 @@ def display_results(results: Sequence[ns], args: ns): setattr(axes, type, axs[idx]) idx += 1 + Ymin = min([min(r.Y) for r in results]) + Ymax = max([max(r.Y) for r in results]) + ymin = (Ymin * 100) // 10 / 10 + ymax = (Ymax * 100 + 9) // 10 / 10 + if args.pmf: for res in results: draw_pmf(axes.pmf, res.Y, label=res.label) @@ -134,10 +150,18 @@ def display_results(results: Sequence[ns], args: ns): if args.cor: assert len(results) >= 2 + if args.diag: + draw_diag(axes.cor, ymin, ymax) ref = results[0] for res in results[1:]: - print("XXX", len(ref.Y), len(res.Y)) - draw_cor(axes.cor, ref.Y, res.Y, ref_label=ref.label, label=res.label) + draw_cor( + axes.cor, + ref.Y, + res.Y, + ref_label=ref.label, + label=res.label, + alpha=args.alpha, + ) axes.cor.legend(loc="upper left") axes.cor.set_title("Peak performance correlation") @@ -175,12 +199,24 @@ def main(): default=False, help="draw correlation", ) + parser.add_argument( + "--diag", + action=argparse.BooleanOptionalAction, + default=True, + help="draw diagonal on correlation", + ) parser.add_argument( "--show", action=argparse.BooleanOptionalAction, default=True, help="show figure", ) + parser.add_argument( + "--alpha", + type=float, + default=0.6, + help="correlation alpha", + ) parser.add_argument( "--debug", action=argparse.BooleanOptionalAction, help="debug mode" ) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index fe64d1aff..70d7f11f0 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -7,6 +7,7 @@ #include #include #include "perf_event.h" +#include "perf_metrics.h" extern double fclock(void); /* from fclock.c */ @@ -124,7 +125,7 @@ typedef int (*packed_func_t)(PackedArg *, int *, int, PackedArg *, int *); CLOSE_PERF_EVENTS(events_num, perf_fds); \ } -void evaluate_packed_perf(double *results, int events_num, const char *events_names[], +static void run_evaluate_packed(double *results, int events_num, const char *events_names[], int repeat, int number, int min_repeat_ms, packed_func_t func, PackedArg *args, int *codes, int nargs) { @@ -183,51 +184,227 @@ void evaluate6_perf(double *results, int events_num, const char *events_names[], define_evaluateN(func, arg0, arg1, arg2, arg3, arg4, arg5); } +typedef enum { + EXEC_UNPACKED, + EXEC_PACKED +} exec_type_t; + +typedef struct { + exec_type_t type; + union { + struct { + void (*func)(); + void **args; + int nargs; + } unpacked; + struct { + packed_func_t func; + PackedArg *args; + int *codes; + int nargs; + } packed; + }; +} exec_context_t; + + +static void dispatch_execution(double *results, int events_num, const char *events_names[], + int repeat, int number, int min_repeat_ms, + exec_context_t *ctx) +{ + if (ctx->type == EXEC_UNPACKED) { + switch (ctx->unpacked.nargs) { + case 0: evaluate0_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func0_t)ctx->unpacked.func); break; + case 1: evaluate1_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func1_t)ctx->unpacked.func, ctx->unpacked.args[0]); break; + case 2: evaluate2_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func2_t)ctx->unpacked.func, ctx->unpacked.args[0], ctx->unpacked.args[1]); break; + case 3: evaluate3_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func3_t)ctx->unpacked.func, ctx->unpacked.args[0], ctx->unpacked.args[1], ctx->unpacked.args[2]); break; + case 4: evaluate4_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func4_t)ctx->unpacked.func, ctx->unpacked.args[0], ctx->unpacked.args[1], ctx->unpacked.args[2], ctx->unpacked.args[3]); break; + case 5: evaluate5_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func5_t)ctx->unpacked.func, ctx->unpacked.args[0], ctx->unpacked.args[1], ctx->unpacked.args[2], ctx->unpacked.args[3], ctx->unpacked.args[4]); break; + case 6: evaluate6_perf(results, events_num, events_names, repeat, number, min_repeat_ms, (func6_t)ctx->unpacked.func, ctx->unpacked.args[0], ctx->unpacked.args[1], ctx->unpacked.args[2], ctx->unpacked.args[3], ctx->unpacked.args[4], ctx->unpacked.args[5]); break; + default: assert(0); break; + } + } else { + run_evaluate_packed(results, events_num, events_names, repeat, number, min_repeat_ms, + ctx->packed.func, ctx->packed.args, ctx->packed.codes, ctx->packed.nargs); + } +} + +typedef struct { + int is_derived; + metric_resolver_t resolver; + int hw_offset; + int out_offset; + int start_pass; +} metric_map_t; + + +static void evaluate_perf_multipass_engine(double *results, int events_num, const char *events_names[], + int repeat, int number, int min_repeat_ms, + exec_context_t *ctx) +{ + // No event only time + if (events_num == 0) { + dispatch_execution(results, 0, NULL, repeat, number, min_repeat_ms, ctx); + return; + } + + int max_passes = 1; + int total_hw_events = 0; + int total_out_results = 0; + int has_standard_pmus = 0; + metric_map_t *map = (metric_map_t *)malloc(events_num * sizeof(metric_map_t)); + + for (int i = 0; i < events_num; i++) { + map[i].hw_offset = total_hw_events; + map[i].out_offset = total_out_results; + + if (resolve_metric(events_names[i], &map[i].resolver) && map[i].resolver.is_supported) { + map[i].is_derived = 1; + + // isolate the TMA within it own passes + map[i].start_pass = max_passes; + max_passes += map[i].resolver.num_passes; + + total_hw_events += map[i].resolver.num_hw_events; + total_out_results += map[i].resolver.num_results; + } else { + // All PMUs in pass 0 + map[i].is_derived = 0; + map[i].start_pass = 0; + has_standard_pmus = 1; + + total_hw_events += 1; + total_out_results += 1; + } + } + + double *hw_results = (double *)malloc(repeat * total_hw_events * sizeof(double)); + + for (int pass = 0; pass < max_passes; pass++) { + // No PMU, pass 0 is reserved to PMU + if (pass == 0 && !has_standard_pmus) continue; + + int pass_events_num = 0; + const char **pass_events_names = (const char **)malloc(total_hw_events * sizeof(char *)); + int *pass_hw_offsets = (int *)malloc(total_hw_events * sizeof(int)); + + for (int i = 0; i < events_num; i++) { + if (map[i].is_derived) { + // If current pass is owned by this TMA + if (pass >= map[i].start_pass && pass < map[i].start_pass + map[i].resolver.num_passes) { + int local_pass = pass - map[i].start_pass; + + int ev_start = 0; + for (int p = 0; p < local_pass; p++) ev_start += map[i].resolver.events_per_pass[p]; + int ev_count = map[i].resolver.events_per_pass[local_pass]; + + for (int j = 0; j < ev_count; j++) { + pass_events_names[pass_events_num] = map[i].resolver.hw_events[ev_start + j]; + pass_hw_offsets[pass_events_num] = map[i].hw_offset + ev_start + j; + pass_events_num++; + } + } + } else { + if (pass == 0) { + pass_events_names[pass_events_num] = events_names[i]; + pass_hw_offsets[pass_events_num] = map[i].hw_offset; + pass_events_num++; + } + } + } + + if (pass_events_num > 0) { + double *pass_results = (double *)malloc(repeat * pass_events_num * sizeof(double)); + + int all_failed = 1; + perf_event_args_t *test_args = (perf_event_args_t *)malloc(pass_events_num * sizeof(perf_event_args_t)); + int *test_fds = (int *)malloc(pass_events_num * sizeof(int)); + + for (int j = 0; j < pass_events_num; j++) GET_PERF_EVENT_CONFIG(pass_events_names[j], &test_args[j]); + OPEN_PERF_EVENTS(pass_events_num, test_args, test_fds); + + for (int j = 0; j < pass_events_num; j++) if (test_fds[j] >= 0) all_failed = 0; + + for (int j = 0; j < pass_events_num; j++) PERF_EVENT_ARGS_DESTROY(test_args[j]); + CLOSE_PERF_EVENTS(pass_events_num, test_fds); + free(test_args); + free(test_fds); + + if (all_failed) { + fprintf(stderr,"[DEBUG] execution bypassed all events failed for the pass %d\n",pass); + // Bypass heavy kernel execution if counters failed + for (int r = 0; r < repeat; r++) { + for (int j = 0; j < pass_events_num; j++) pass_results[r * pass_events_num + j] = -1.0; + } + } else { + dispatch_execution(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, ctx); + } + + // Gather results from passes + for (int r = 0; r < repeat; r++) { + for (int e = 0; e < pass_events_num; e++) { + hw_results[r * total_hw_events + pass_hw_offsets[e]] = + pass_results[r * pass_events_num + e]; + } + } + free(pass_results); + } + free(pass_events_names); + free(pass_hw_offsets); + } + + // Postprocessing + for (int r = 0; r < repeat; r++) { + for (int i = 0; i < events_num; i++) { + const double *run_raw = &hw_results[r * total_hw_events + map[i].hw_offset]; + double *run_final = &results[r * total_out_results + map[i].out_offset]; + + if (map[i].is_derived) { + int failed = 0; + for (int ev = 0; ev < map[i].resolver.num_hw_events; ev++) { + if (run_raw[ev] == -1.0) failed = 1; + } + if (failed) { + for (int res_idx = 0; res_idx < map[i].resolver.num_results; res_idx++) { + run_final[res_idx] = -1.0; + } + } else { + map[i].resolver.compute_formula(run_raw, run_final); + } + } else { + run_final[0] = run_raw[0]; + } + } + } + + free(hw_results); + free(map); +} void evaluate_perf(double *results, int events_num, const char *events_names[], int repeat, int number, int min_repeat_ms, void (*func)(), void **args, int nargs) { - switch (nargs) { - case 0: - evaluate0_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func0_t)func); - break; - case 1: - evaluate1_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func1_t)func, args[0]); - break; - case 2: - evaluate2_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func2_t)func, args[0], args[1]); - break; - case 3: - evaluate3_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func3_t)func, args[0], args[1], args[2]); - break; - case 4: - evaluate4_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func4_t)func, args[0], args[1], args[2], args[3]); - break; - case 5: - evaluate5_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func5_t)func, args[0], args[1], args[2], args[3], args[4]); - break; - case 6: - evaluate6_perf(results, events_num, events_names, - repeat, number, min_repeat_ms, - (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); - break; - default: - assert(0); - break; - } + exec_context_t ctx; + ctx.type = EXEC_UNPACKED; + ctx.unpacked.func = func; + ctx.unpacked.args = args; + ctx.unpacked.nargs = nargs; + + evaluate_perf_multipass_engine(results, events_num, events_names, repeat, number, min_repeat_ms, &ctx); +} + +void evaluate_packed_perf(double *results, int events_num, const char *events_names[], + int repeat, int number, int min_repeat_ms, + packed_func_t func, PackedArg *args, int *codes, int nargs) +{ + exec_context_t ctx; + ctx.type = EXEC_PACKED; + ctx.packed.func = func; + ctx.packed.args = args; + ctx.packed.codes = codes; + ctx.packed.nargs = nargs; + + evaluate_perf_multipass_engine(results, events_num, events_names, repeat, number, min_repeat_ms, &ctx); } void evaluate(double *results, @@ -246,3 +423,12 @@ void evaluate_packed(double *results, repeat, number, min_repeat_ms, func, args, codes, nargs); } + +int get_total_results_size(int events_num, const char *events_names[]) +{ + int total = 0; + for (int i = 0; i < events_num; i++) { + total += get_perf_metric_results_count(events_names[i]); + } + return total; +} diff --git a/src/xtc/csrcs/runtimes/host/perf_event.h b/src/xtc/csrcs/runtimes/host/perf_event.h index 0b4157e9c..aaa66742f 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event.h +++ b/src/xtc/csrcs/runtimes/host/perf_event.h @@ -29,7 +29,7 @@ typedef struct { int type; - int event; + uint64_t event; } perf_event_type_event_t; typedef enum { diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 3d28d0f82..080477a28 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -81,7 +81,7 @@ static void init_perf_event_attr(struct perf_event_attr *attr_ptr) } -int open_perf_event(perf_event_args_t event) { +static int open_perf_event_group(perf_event_args_t event, int group_fd) { if (event.mode == PERF_ARG_INVALID) { return -1; } else if (event.mode == PERF_ARG_GENERIC) { @@ -89,16 +89,19 @@ int open_perf_event(perf_event_args_t event) { init_perf_event_attr(&attr); attr.type = event.args.config_pair.type; attr.config = event.args.config_pair.event; - return sys_perf_event_open(&attr, 0 /*pid*/, -1 /*cpu*/, -1 /*group_fd*/, - 0 /*flags*/); + return sys_perf_event_open(&attr, 0 /*pid*/, -1 /*cpu*/, group_fd /*group fd*/, 0 /*flags*/); } else { local_pfm_perf_encode_arg_t *perf_gen = (local_pfm_perf_encode_arg_t *)event.args.config_ptr; return sys_perf_event_open(perf_gen->attr, - 0 /*pid*/, perf_gen->cpu /*cpu*/, -1 /*group_fd*/, + 0 /*pid*/, perf_gen->cpu /*cpu*/, group_fd /*group fd*/, perf_gen->flags /*flags*/); } } +int open_perf_event(perf_event_args_t event) { + return open_perf_event_group(event, -1); +} + static __attribute__((constructor)) void perf_event_init(void) { #if HAS_PFM int res = pfm_initialize(); @@ -128,12 +131,16 @@ void close_perf_event(int perf_fd) { close(perf_fd); } void open_perf_events(int n_events, const perf_event_args_t *events, int *fds) { assert(n_events <= PERF_EVENT_MAX_EVENTS); + int group_fd = -1; // group leader for (int i = 0; i < n_events; i++) { #if HAS_GPU if (events[i].mode == PERF_ARG_GPU) // FIXME do it more efficiently continue; #endif /* HAS_GPU */ - fds[i] = open_perf_event(events[i]); + fds[i] = open_perf_event_group(events[i], group_fd); + if (group_fd == -1 && fds[i] >= 0) { + group_fd = fds[i]; + } } #if HAS_GPU open_perf_events__gpu(n_events, events, fds); @@ -205,6 +212,186 @@ void stop_perf_events(int n_events, const int *fds, uint64_t *results) { #endif /* HAS_GPU */ } +#define X86_RAW(event, umask, cmask) \ + ((((uint64_t)(event) >> 8) << 32) | \ + ((uint64_t)(cmask) << 24) | \ + ((uint64_t)(umask) << 8) | \ + ((uint64_t)(event) & 0xFF)) + +typedef struct { + const char *name; + uint64_t raw_config; +} pmu_event_def_t; + +// INTEL SKYLAKE / CASCADE LAKE (Pre-Ice Lake, no hw PERF_METRICS) +static const pmu_event_def_t skl_raw_events[] = { + { "@skl_slots", X86_RAW(0x3C, 0x00, 0) }, + { "@skl_fe_bound", X86_RAW(0x9C, 0x01, 0) }, + { "@skl_issued", X86_RAW(0x0E, 0x01, 0) }, + { "@skl_retiring", X86_RAW(0xC2, 0x02, 0) }, + // with flags (any=1, edge=1) + { "@skl_recovery", 0x0120010D }, + { "@skl_machine_clears", 0x010401C3 }, + // L2 & L3 Memory Bound + { "@skl_mem_stalls", X86_RAW(0xA3, 0x14, 0x14) }, + { "@skl_core_stalls", X86_RAW(0xA2, 0x01, 0) }, + { "@skl_fetch_lat_data", X86_RAW(0x80, 0x04, 0) }, + { "@skl_fetch_lat_tag", X86_RAW(0x83, 0x04, 0) }, + { "@skl_heavy_ops", X86_RAW(0x79, 0x30, 0) }, + { "@skl_br_misp", X86_RAW(0xC5, 0x00, 0) }, + { "@skl_stalls_mem_any", X86_RAW(0xA3, 0x14, 0x14) }, + { "@skl_stalls_l1d_miss", X86_RAW(0xA3, 0x0C, 0x0C) }, + { "@skl_stalls_l2_miss", X86_RAW(0xA3, 0x05, 0x05) }, + { "@skl_stalls_l3_miss", X86_RAW(0xA3, 0x06, 0x06) }, + { "@skl_bound_on_stores", X86_RAW(0xA6, 0x40, 0) }, + // L3 Additional Metrics (Execution, Frontend, FPU) + { "@skl_divider_active", X86_RAW(0x14, 0x14, 0x01) }, + { "@skl_scoreboard", X86_RAW(0xA2, 0x02, 0) }, + { "@skl_icache_miss", X86_RAW(0x83, 0x02, 0) }, + { "@skl_itlb_miss", X86_RAW(0x85, 0x0E, 0) }, + { "@skl_clear_resteer", X86_RAW(0x0D, 0x80, 0) }, + { "@skl_lcp", X86_RAW(0x87, 0x01, 0) }, + { "@skl_dsb2mite", X86_RAW(0xAB, 0x02, 0) }, + { "@skl_ms_switches", X86_RAW(0x79, 0x06, 0x01) }, + { "@skl_idq_mite", X86_RAW(0x79, 0x04, 0) }, + { "@skl_idq_dsb", X86_RAW(0x79, 0x08, 0) }, + { "@skl_idq_ms", X86_RAW(0x79, 0x30, 0) }, // Alias of heavy_ops + { "@skl_macro_fused", X86_RAW(0xC2, 0x04, 0) }, + { "@skl_mem_inst", X86_RAW(0xD0, 0x81, 0) }, + { "@skl_br_inst", X86_RAW(0xC4, 0x00, 0) }, + { "@skl_nukes_mem", 0x010402C3 }, // MACHINE_CLEARS.MEMORY_ORDERING (cmask=1, edge=1) + // L3 FPU / AVX + { "@skl_fp_scalar_s", X86_RAW(0xC7, 0x02, 0) }, + { "@skl_fp_scalar_d", X86_RAW(0xC7, 0x01, 0) }, + { "@skl_fp_128_s", X86_RAW(0xC7, 0x08, 0) }, + { "@skl_fp_128_d", X86_RAW(0xC7, 0x04, 0) }, + { "@skl_fp_256_s", X86_RAW(0xC7, 0x20, 0) }, + { "@skl_fp_256_d", X86_RAW(0xC7, 0x10, 0) }, + { "@skl_fp_512_s", X86_RAW(0xC7, 0x80, 0) }, + { "@skl_fp_512_d", X86_RAW(0xC7, 0x40, 0) } +}; + +// INTEL MODERN (Ice Lake, Sapphire Rapids, Alder/Raptor Lake) +static const pmu_event_def_t icl_raw_events[] = { + { "@icl_scoreboard", X86_RAW(0x00, 0x00, 0) }, // Always return 0 + // L1 & L2 perf metrics + { "@icl_slots", X86_RAW(0x00, 0x04, 0) }, // 0x0400 + { "@icl_retiring", X86_RAW(0x00, 0x80, 0) }, // 0x8000 + { "@icl_bad_spec", X86_RAW(0x00, 0x81, 0) }, // 0x8100 + { "@icl_fe_bound", X86_RAW(0x00, 0x82, 0) }, // 0x8200 + { "@icl_be_bound", X86_RAW(0x00, 0x83, 0) }, // 0x8300 + { "@icl_heavy_ops", X86_RAW(0x00, 0x84, 0) }, // 0x8400 + { "@icl_br_mispredict", X86_RAW(0x00, 0x85, 0) }, // 0x8500 + { "@icl_fetch_lat", X86_RAW(0x00, 0x86, 0) }, // 0x8600 + { "@icl_mem_bound", X86_RAW(0x00, 0x87, 0) }, // 0x8700 + // L3 Memory Bound (same as Skylake) + { "@icl_cyc", X86_RAW(0x3C, 0x00, 0) }, + { "@icl_stalls_mem_any", X86_RAW(0xA3, 0x14, 0x14) }, + { "@icl_stalls_l1d_miss", X86_RAW(0xA3, 0x0C, 0x0C) }, + { "@icl_stalls_l2_miss", X86_RAW(0xA3, 0x05, 0x05) }, + { "@icl_stalls_l3_miss", X86_RAW(0xA3, 0x06, 0x06) }, + { "@icl_bound_on_stores", X86_RAW(0xA6, 0x40, 0) }, + // L3 Execution, Frontend, Retiring + { "@icl_core_stalls", X86_RAW(0xA6, 0x01, 0) }, // EXE_ACTIVITY.EXE_BOUND_0_PORTS + { "@icl_divider_active", X86_RAW(0x14, 0x14, 0x01) }, + { "@icl_icache_miss", X86_RAW(0x80, 0x04, 0) }, // ICACHE_16B.IFDATA_STALL + { "@icl_itlb_miss", X86_RAW(0x83, 0x04, 0) }, // ICACHE_64B.IFTAG_STALL + { "@icl_clear_resteer", X86_RAW(0x0D, 0x80, 0) }, + { "@icl_lcp", X86_RAW(0x87, 0x01, 0) }, + { "@icl_dsb2mite", X86_RAW(0xAB, 0x02, 0) }, + { "@icl_ms_switches", X86_RAW(0x79, 0x06, 0x01) }, + { "@icl_idq_mite", X86_RAW(0x79, 0x04, 0) }, + { "@icl_idq_dsb", X86_RAW(0x79, 0x08, 0) }, + { "@icl_idq_ms", X86_RAW(0x79, 0x30, 0) }, + { "@icl_macro_fused", X86_RAW(0xC2, 0x04, 0) }, + { "@icl_mem_inst", X86_RAW(0xD0, 0x81, 0) }, + { "@icl_br_inst", X86_RAW(0xC4, 0x00, 0) }, + // L3 Parents (Other) + { "@icl_retiring_uops", X86_RAW(0xC2, 0x02, 0) }, + { "@icl_issued_any", X86_RAW(0x0E, 0x01, 0) }, + { "@icl_br_misp", X86_RAW(0xC5, 0x00, 0) }, + { "@icl_nukes_mem", 0x010402C3 }, + // L3 FPU / AVX (same as Skylake) + { "@icl_fp_scalar_s", X86_RAW(0xC7, 0x02, 0) }, + { "@icl_fp_scalar_d", X86_RAW(0xC7, 0x01, 0) }, + { "@icl_fp_128_s", X86_RAW(0xC7, 0x08, 0) }, + { "@icl_fp_128_d", X86_RAW(0xC7, 0x04, 0) }, + { "@icl_fp_256_s", X86_RAW(0xC7, 0x20, 0) }, + { "@icl_fp_256_d", X86_RAW(0xC7, 0x10, 0) }, + { "@icl_fp_512_s", X86_RAW(0xC7, 0x80, 0) }, + { "@icl_fp_512_d", X86_RAW(0xC7, 0x40, 0) } +}; + +// AMD ZEN 4 (Family 19h) +static const pmu_event_def_t zen4_raw_events[] = { + { "@zen4_cyc", X86_RAW(0x076, 0x00, 0) }, // LS_NOT_HALTED_CYC: Core active cycles + { "@zen4_fe", X86_RAW(0x1A0, 0x01, 0) }, // DE_NO_DISPATCH_PER_SLOT.NO_OPS_FROM_FRONTEND: Dispatch slots empty due to FE + { "@zen4_disp", X86_RAW(0x0AA, 0x07, 0) }, // DE_SRC_OP_DISP.ALL: OPs dispatched from any source (Decoder, OpCache, uCode) + { "@zen4_ret", X86_RAW(0x0C1, 0x00, 0) }, // EX_RET_OPS: Total OPs retired + { "@zen4_be_mem", X86_RAW(0x1A0, 0x02, 0) }, + { "@zen4_be_cpu", X86_RAW(0x1A0, 0x04, 0) }, + { "@zen4_fe_lat", X86_RAW(0x1A0, 0x01, 0x06) }, // DE_NO_DISPATCH_PER_SLOT.NO_OPS_FROM_FRONTEND (Cmask=6): Full pipeline stall from FE + { "@zen4_fe_tot", X86_RAW(0x1A0, 0x01, 0) }, + { "@zen4_bs_misp", X86_RAW(0x0C3, 0x00, 0) }, + { "@zen4_bs_resync", X86_RAW(0x091, 0x00, 0) }, + { "@zen4_ret_micro", X86_RAW(0x1C2, 0x00, 0) } // EX_RET_UCODE_OPS: Retired macro-ops originating from microcode sequencer (Heavy Ops) +}; + +#define ARM_RAW(event) (event) + +static const pmu_event_def_t arm_raw_events[] = { + // TopdownL1 + { "@arm_cyc", ARM_RAW(0x11) }, // CPU_CYCLES + { "@arm_fe", ARM_RAW(0x23) }, // STALL_FRONTEND + { "@arm_be", ARM_RAW(0x24) }, // STALL_BACKEND + { "@arm_inst", ARM_RAW(0x08) }, // INST_RETIRED + { "@arm_brmisp", ARM_RAW(0x10) }, // BR_MIS_PRED + + // TopdownL2 + { "@arm_be_mem", ARM_RAW(0x45) }, // STALL_BACKEND_MEM + { "@arm_be_cpu", ARM_RAW(0x44) }, // STALL_BACKEND_CPU + { "@arm_l1i_miss", ARM_RAW(0x01) } // L1I_CACHE_REFILL +}; + +/* + * Source + * Intel : https://github.com/torvalds/linux/blob/m aster/arch/x86/events/intel/core.c + * AMD : https://github.com/torvalds/linux/blob/m aster/arch/x86/events/amd/core.c + * tools/perf/pmu-events/arch/x86/amdzen4/pipeline.json + * + * https://github.com/intel/perfmon + * + * todo ARM : https://developer.arm.com/documentation/ddi0434/a/performance-monitoring-unit/performance-monitoring-register-descriptions/event-type-select-register?lang=en + */ +static inline int find_raw_event(const char *name, const pmu_event_def_t *table, int table_size, perf_event_args_t *event) { + for (int i = 0; i < table_size; i++) { + if (strcmp(name, table[i].name) == 0) { + event->mode = PERF_ARG_GENERIC; + event->args.config_pair.type = PERF_TYPE_RAW; + event->args.config_pair.event = table[i].raw_config; + return 0; + } + } + return 1; +} + +static inline int set_config_by_arch(const char *name, perf_event_args_t *event) { + // Old Intel (Skylake / Cascade Lake) + if (strncmp(name, "@skl_", 5) == 0) { + return find_raw_event(name, skl_raw_events, sizeof(skl_raw_events) / sizeof(skl_raw_events[0]), event); + } + // Modern Intel (Ice Lake / Raptor Lake) + else if (strncmp(name, "@icl_", 5) == 0) { + return find_raw_event(name, icl_raw_events, sizeof(icl_raw_events) / sizeof(icl_raw_events[0]), event); + } + // AMD Zen 4 + else if (strncmp(name, "@zen4_", 6) == 0) { + return find_raw_event(name, zen4_raw_events, sizeof(zen4_raw_events) / sizeof(zen4_raw_events[0]), event); + } + + return -1; // unknow prefix +} + int get_perf_event_config(const char *name, perf_event_args_t *event) { for (int e = 0; e < sizeof(perf_events_decl) / sizeof(*perf_events_decl); e++) { @@ -215,13 +402,16 @@ int get_perf_event_config(const char *name, perf_event_args_t *event) { return 0; } } - - #if HAS_GPU + + int arch_specific_config = set_config_by_arch(name,event); + if(arch_specific_config != -1) return arch_specific_config; + +#if HAS_GPU if (strncmp(name, "gpu.", 4) == 0) { return get_perf_event_config__gpu(name, event); } - #endif /* HAS_GPU */ - +#endif /* HAS_GPU */ + #if HAS_PFM struct perf_event_attr *attr = malloc(sizeof(struct perf_event_attr)); init_perf_event_attr(attr); @@ -253,6 +443,6 @@ void perf_event_args_destroy(perf_event_args_t args) { local_pfm_perf_encode_arg_t* pfm = (local_pfm_perf_encode_arg_t*) args.args.config_ptr; free((void*)pfm->attr); free((void*)args.args.config_ptr); - args.args.config_ptr = NULL; - } + args.args.config_ptr = NULL; + } } diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c new file mode 100644 index 000000000..317219ca0 --- /dev/null +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -0,0 +1,933 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2024-2026 The XTC Project Authors + */ +#include +#include +#include +#include +//#include + +#include "perf_metrics.h" + + +#if defined(__x86_64__) || defined(__i386__) + #define ARCH_IS_X86 1 + #include + #include +#else + #define ARCH_IS_X86 0 +#endif + +#if defined(__aarch64__) || defined(__arm__) + #define ARCH_IS_ARM 1 +#else + #define ARCH_IS_ARM 0 +#endif + +#if ARCH_IS_X86 + +#define GET_METRIC(m, i) (((m) >> (i*8)) & 0xff) + +/* L1 Topdown metric events */ +#define TOPDOWN_RETIRING(val) ((float)GET_METRIC(val, 0) / 0xff) +#define TOPDOWN_BAD_SPEC(val) ((float)GET_METRIC(val, 1) / 0xff) +#define TOPDOWN_FE_BOUND(val) ((float)GET_METRIC(val, 2) / 0xff) +#define TOPDOWN_BE_BOUND(val) ((float)GET_METRIC(val, 3) / 0xff) + +/* + * L2 Topdown metric events. + * Available on Sapphire Rapids and later platforms. + */ +#define TOPDOWN_HEAVY_OPS(val) ((float)GET_METRIC(val, 4) / 0xff) +#define TOPDOWN_BR_MISPREDICT(val) ((float)GET_METRIC(val, 5) / 0xff) +#define TOPDOWN_FETCH_LAT(val) ((float)GET_METRIC(val, 6) / 0xff) +#define TOPDOWN_MEM_BOUND(val) ((float)GET_METRIC(val, 7) / 0xff) + +#define RDPMC_FIXED (1 << 30) /* return fixed counters */ +#define RDPMC_METRIC (1 << 29) /* return metric counters */ + +#define FIXED_COUNTER_SLOTS 4 +#define METRIC_COUNTER_TOPDOWN_L1_L2 0 + +static inline uint64_t read_slots(void) +{ + return _rdpmc(RDPMC_FIXED | FIXED_COUNTER_SLOTS); +} + +static inline uint64_t read_metrics(void) +{ + return _rdpmc(RDPMC_METRIC | METRIC_COUNTER_TOPDOWN_L1_L2); +} +/* +_rdpmc calls should not be mixed with reading the metrics and slots counters +through system calls, as the kernel will reset these counters after each system +call. */ + + + +#ifdef __linux__ +#include +static void get_cpu_family_model(int *family, int *model) { + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + + if (__get_cpuid(1, &eax, &ebx, &ecx, &edx)) { + int base_family = (eax >> 8) & 0xF; + int base_model = (eax >> 4) & 0xF; + + int ext_family = (eax >> 20) & 0xFF; + int ext_model = (eax >> 16) & 0xF; + + *family = base_family; + *model = base_model; + + if (base_family == 0xF) { + *family += ext_family; + } + if (base_family == 0x6 || base_family == 0xF) { + *model += (ext_model << 4); + } + } else { + *family = 0; + *model = 0; + } +} +#endif + +typedef enum { + UARCH_UNKNOWN = 0, + UARCH_INTEL_OLD, // No native tma counters + UARCH_INTEL_MODERN, // Native tma counters + UARCH_AMD_ZEN_1_2, + UARCH_AMD_ZEN_3, + UARCH_AMD_ZEN_4, + UARCH_ARM +} cpu_uarch_t; + +static cpu_uarch_t current_uarch = UARCH_UNKNOWN; +static int is_uarch_initialized = 0; + +static cpu_uarch_t detect_hardware_architecture(void) { +#if ARCH_IS_X86 + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + if (!__get_cpuid(0, &eax, &ebx, &ecx, &edx)) return UARCH_UNKNOWN; + + int family = 0, model = 0; + get_cpu_family_model(&family, &model); + + // Intel: ebx="Genu", edx="ineI", ecx="ntel" + if (ebx == 0x756e6547 && edx == 0x49656e69 && ecx == 0x6c65746e) { + if (family == 6) { + switch (model) { + case 0x4E: case 0x5E: case 0x55: // Skylake, Cascade Lake, Cooper Lake + case 0x8E: case 0x9E: // Kaby, Coffee, Whiskey, Amber Lake + case 0xA5: case 0xA6: // Comet Lake + case 0x3D: case 0x47: case 0x4F: // Broadwell + case 0x56: // Broadwell-DE + case 0x66: // Cannon Lake + return UARCH_INTEL_OLD; + + case 0x7E: case 0x7D: case 0x9D: // Ice Lake Client + case 0x6A: case 0x6C: // Ice Lake Server + case 0xA7: // Rocket Lake + case 0x8C: case 0x8D: // Tiger Lake + case 0x8A: // Lakefield + case 0x8F: // Sapphire Rapids + case 0xCF: // Emerald Rapids + case 0xAD: case 0xAE: // Granite Rapids + case 0x97: case 0x9A: // Alder Lake + case 0xB7: case 0xBA: case 0xBF: // Raptor Lake + case 0xD7: // Bartlett Lake + case 0xAA: case 0xAC: // Meteor Lake + case 0xB5: case 0xC5: case 0xC6: // Arrow Lake + case 0xBD: // Lunar Lake + case 0xCC: case 0xE5: // Panther Lake + case 0xD5: // Wildcat Lake + return UARCH_INTEL_MODERN; + } + } + else if (family == 18) { + switch (model) { + case 0x01: case 0x03: + return UARCH_INTEL_MODERN; // Nova Lake + } + } + else if (family == 19) { + switch (model) { + case 0x01: + return UARCH_INTEL_MODERN; // Diamond Rapids + } + } + } + // AMD: ebx="Auth", edx="enti", ecx="cAMD" + else if (ebx == 0x68747541 && edx == 0x69746e65 && ecx == 0x444d4163) { + if (family == 0x17) { + return UARCH_AMD_ZEN_1_2; // Zen 1 (Naples, Summit Ridge), Zen+ (Pinnacle), Zen 2 (Rome, Matisse) + } + else if (family == 0x19) { + if (model >= 0x10 && model <= 0x1F) return UARCH_AMD_ZEN_4; // Genoa + if (model >= 0x60 && model <= 0x7F) return UARCH_AMD_ZEN_4; // Phoenix, Dragon Range + if (model >= 0xA0 && model <= 0xAF) return UARCH_AMD_ZEN_4; // Bergamo + + return UARCH_AMD_ZEN_3; // Milan, Vermeer... (Default for family 19h not Zen 4) + } + // TODO: add family 0x1A pour Zen 5 (Turin, Granite Ridge) + } +#elif ARCH_IS_ARM + return UARCH_ARM; // need sysfs for more info +#endif + return UARCH_UNKNOWN; +} + +static inline cpu_uarch_t get_current_uarch(void) { + if (!is_uarch_initialized) { + current_uarch = detect_hardware_architecture(); + is_uarch_initialized = 1; + } + return current_uarch; +} + +// Skylake arch + +static const char *skl_tma_l1_events[] = { + "@skl_slots", // 0x003c (Cycles) + "@skl_fe_bound", // 0x019c + "@skl_issued", // 0x010e + "@skl_retiring", // 0x02c2 + "@skl_recovery" // 0x0100019d +}; + +static void compute_skl_tma_l1(const double *raw_values, double *final_results) { + double cycles = raw_values[0]; + double fe_bound_raw = raw_values[1]; + double issued_raw = raw_values[2]; + double retiring_raw = raw_values[3]; + double bad_spec_raw = raw_values[4]; + + // Pipeline width = 4 on Intel Skylake + double slots = 4.0 * cycles; + + if (slots > 0) { + double r_fe_bound = fe_bound_raw / slots; + double r_retiring = retiring_raw / slots; + double r_bad_spec = bad_spec_raw / slots; + double r_be_bound = (slots - retiring_raw - fe_bound_raw - bad_spec_raw) / slots; + + final_results[0] = r_retiring * 100.0; + final_results[1] = r_bad_spec * 100.0; + final_results[2] = r_fe_bound * 100.0; + final_results[3] = r_be_bound * 100.0; + } else { + final_results[0] = final_results[1] = final_results[2] = final_results[3] = 0.0; + } +} + +static const int skl_l2_passes[] = {4, 4, 4}; // 3 passes of 4 counters +static const char *skl_tma_l2_events[] = { + // Pass 1 : L1 Base + Backend + "@skl_slots", + "cycle_activity:stalls_mem_any", // Memory Bound + "resource_stalls:any", // Core Bound base + "@skl_fe_bound", + + // Pass 2 : Frontend + "@skl_slots", + "icache_16b:ifdata_stall", // Fetch Latency + "icache_64b:iftag_stall", // Fetch Latency + "@skl_retiring", + + // Pass 3 : Retiring + Bad Speculation + "@skl_slots", + "idq:ms_uops", // Heavy Ops + "br_misp_retired:all_branches", // Branch Mispredicts + "machine_clears:count" // Machine Clears +}; + + +static void compute_skl_tma_l2(const double *raw, double *final) { + // Pass 1 : 0 (slots), 1 (mem), 2 (core), 3 (fe) + // Pass 2 : 4 (slots), 5 (lat_d), 6 (lat_t), 7 (ret) + // Pass 3 : 8 (slots), 9 (heavy), 10 (misp), 11 (clears) + + // Use the slot of each group to not mess up ratio + double slots_p1 = raw[0] * 4.0; + double slots_p2 = raw[4] * 4.0; + double slots_p3 = raw[8] * 4.0; + + if (slots_p1 > 0 && slots_p2 > 0 && slots_p3 > 0) { + // Frontend + double fe_bound = raw[3] / slots_p1; + double fetch_lat = (raw[5] + raw[6]) / slots_p2; + double fetch_bw = fe_bound - fetch_lat; + + // Retiring + double retiring = raw[7] / slots_p2; + double heavy_ops = raw[9] / slots_p3; + double light_ops = retiring - heavy_ops; + + // Bad Speculation + double br_misp = raw[10] / slots_p3; + double m_clears = raw[11] / slots_p3; + double bad_spec = br_misp + m_clears; + + // Backend + double mem_bound = raw[1] / slots_p1; + double core_bound = raw[2] / slots_p1; + + // Global backend + double be_bound = 1.0 - (fe_bound + bad_spec + retiring); + double total_be_raw = mem_bound + core_bound; + if (total_be_raw > 0) { + mem_bound = be_bound * (mem_bound / total_be_raw); + core_bound = be_bound * (core_bound / total_be_raw); + } else { + mem_bound = be_bound; core_bound = 0; + } + + final[0] = light_ops * 100.0; + final[1] = heavy_ops * 100.0; + final[2] = m_clears * 100.0; + final[3] = br_misp * 100.0; + final[4] = fetch_bw * 100.0; + final[5] = fetch_lat * 100.0; + final[6] = core_bound * 100.0; + final[7] = mem_bound * 100.0; + + for(int i=0; i<8; i++) if (final[i] < 0.0) final[i] = 0.0; + } else { + for(int i=0; i<8; i++) final[i] = 0.0; + } +} + +static const int skl_l3_mem_passes[] = {4, 3}; + +static const char *skl_tma_l3_mem_events[] = { + // Pass 1 + "@skl_slots", // + "@skl_stalls_mem_any", // 0x140014a3 + "@skl_stalls_l1d_miss", // 0x0c000c14 (cmask=0x0c, umask=0x0c, event=0x14) + "@skl_stalls_l2_miss", // 0x05000514 (cmask=0x05, umask=0x05, event=0x14) + + // Pass 2 + "@skl_slots", // + "@skl_stalls_l3_miss", // 0x06000614 (cmask=0x06, umask=0x06, event=0x14) + "@skl_bound_on_stores" // 0x08a2 (RESOURCE_STALLS.SB) +}; + +// Compute only memory bound from L3 +static void compute_skl_tma_l3_mem(const double *raw, double *final) { + double cycles_p1 = raw[0]; + double cycles_p2 = raw[4]; + + if (cycles_p1 > 0 && cycles_p2 > 0) { + double mem_any = raw[1] / cycles_p1; + double l1d_miss = raw[2] / cycles_p1; + double l2_miss = raw[3] / cycles_p1; + + double l3_miss = raw[5] / cycles_p2; + double stores = raw[6] / cycles_p2; + + final[0] = (mem_any - l1d_miss) * 100.0; // L1 Bound + final[1] = (l1d_miss - l2_miss) * 100.0; // L2 Bound + final[2] = (l2_miss - l3_miss) * 100.0; // L3 Bound + final[3] = l3_miss * 100.0; // External RAM (DRAM) Bound + final[4] = stores * 100.0; // Store Bound + + for (int i=0; i<5; i++) if (final[i] < 0.0) final[i] = 0.0; + } else { + for (int i=0; i<5; i++) final[i] = 0.0; + } +} + +static const int skl_l3_passes[] = {5, 5, 5, 5, 5, 5, 5, 5}; + +static const char *skl_tma_l3_events[] = { + // Pass 1: Memory Bounds + "@skl_slots", // raw[0] + "@skl_stalls_mem_any", + "@skl_stalls_l1d_miss", + "@skl_stalls_l2_miss", + "@skl_stalls_l3_miss", + + // Pass 2: Execution Stalls + "@skl_slots", // raw[5] + "@skl_divider_active", + "@skl_scoreboard", + "@skl_bound_on_stores", + "@skl_core_stalls", + + // Pass 3: Frontend Latency + "@skl_slots", // raw[10] + "@skl_icache_miss", + "@skl_itlb_miss", + "@skl_clear_resteer", + "@skl_lcp", + + // Pass 4: Frontend Bandwidth + "@skl_slots", // raw[15] + "@skl_dsb2mite", + "@skl_ms_switches", + "@skl_idq_mite", + "@skl_idq_dsb", + + // Pass 5: Instruction Mix + "@skl_slots", // raw[20] + "@skl_idq_ms", + "@skl_macro_fused", + "@skl_mem_inst", + "@skl_br_inst", + + // Pass 6: Floating Point 1 (Scalar & 128b) + "@skl_slots", // raw[25] + "@skl_fp_scalar_s", + "@skl_fp_scalar_d", + "@skl_fp_128_s", + "@skl_fp_128_d", + + // Pass 7: Floating Point 2 (256b & 512b) + "@skl_slots", // raw[30] + "@skl_fp_256_s", + "@skl_fp_256_d", + "@skl_fp_512_s", + "@skl_fp_512_d", + + // Pass 8: Parent Nodes (Other) + "@skl_slots", // raw[35] + "@skl_retiring", // raw[36] (uops_retired.retire_slots) + "@skl_issued", // raw[37] (uops_issued.any) + "@skl_br_misp", // raw[38] (br_misp_retired.all_branches) + "@skl_nukes_mem" // raw[39] (machine_clears.memory_ordering) +}; + +static void compute_skl_tma_l3(const double *raw, double *final) { + double cyc_p1 = raw[0]; double cyc_p2 = raw[5]; double cyc_p3 = raw[10]; + double cyc_p4 = raw[15]; double cyc_p5 = raw[20]; double cyc_p6 = raw[25]; + double cyc_p7 = raw[30]; double cyc_p8 = raw[35]; + + assert(cyc_p1 != cyc_p2 != cyc_p3 != cyc_p4 != cyc_p5 != cyc_p6 != cyc_p7 != cyc_p8 != 0); + if (cyc_p1 > 0 && cyc_p2 > 0 && cyc_p3 > 0 && cyc_p4 > 0 && cyc_p5 > 0 && cyc_p6 > 0 && cyc_p7 > 0 && cyc_p8 > 0) { + + // Memory Subsystem + double l1_bound = (raw[1] - raw[2]) / cyc_p1; + double l2_bound = (raw[2] - raw[3]) / cyc_p1; + double l3_bound = (raw[3] - raw[4]) / cyc_p1; + double dram_bound = raw[4] / cyc_p1; + double store_bnd = raw[8] / cyc_p2; + + // Execution / ALU + double divider = raw[6] / cyc_p2; + double serial = raw[7] / cyc_p2; + double ports_util = (raw[9] - raw[6] - raw[7]) / cyc_p2; // core_stalls - divider - serial + + // Frontend Fetch + double icache = raw[11] / cyc_p3; + double itlb = raw[12] / cyc_p3; + double lcp = raw[14] / cyc_p3; + double dsb2mite = raw[16] / cyc_p4; + double ms_swit = raw[17] / cyc_p4; + double dsb = raw[19] / (cyc_p4 * 4.0); // IDQ.DSB is in uops, divise by slots + double mite = raw[18] / (cyc_p4 * 4.0); // IDQ.MITE is in uops + + // Retiring & Instruction Mix + double ms_uops = raw[21] / (cyc_p5 * 4.0); + double fused = raw[22] / (cyc_p5 * 4.0); + double mem_ops = raw[23] / (cyc_p5 * 4.0); + double non_fused = (raw[24] - raw[22]) / (cyc_p5 * 4.0); // br_inst - fused + + // Floating Point Arithmetic + double fp_arith = (raw[26] + raw[27] + raw[28] + raw[29]) / (cyc_p6 * 4.0) + + (raw[31] + raw[32] + raw[33] + raw[34]) / (cyc_p7 * 4.0); + + // Bad Speculation(L3) + double resteers = raw[13] / cyc_p3; + + // Others + double retiring = raw[36]; + double issued = raw[37]; + double br_misp = raw[38]; + double nukes_mem = raw[39]; + + // Few Uops Instructions + // (Retiring - Fused) / Slots + double few_uops = (retiring - raw[22]) / (cyc_p8 * 4.0); + if (few_uops < 0.0) few_uops = 0.0; + + // Other Light Ops + // Retiring Light - (FP_Arith + Mem_Ops + Fused + Non_Fused) + double ret_light = (retiring - raw[21]) / (cyc_p8 * 4.0); // Retiring - ms_uops + double other_light = ret_light - (fp_arith + mem_ops + fused + non_fused); + if (other_light < 0.0) other_light = 0.0; + + // Other Mispredicts + // Total Bad Speculation - Branch Mispredicts - Machine Clears + double bad_spec_tot = (issued - retiring + (4.0 * raw[13])) / (cyc_p8 * 4.0); // raw[13] = recovery_cycles + double r_br_misp = br_misp / (cyc_p8 * 4.0); + double m_clears = raw[11] / (cyc_p3 * 4.0); // raw[11] (clear_resteer) + double other_misp = bad_spec_tot - r_br_misp - m_clears; + if (other_misp < 0.0) other_misp = 0.0; + + // Other Nukes + // Total Machine Clears - Memory Ordering Clears + double other_nukes = m_clears - (nukes_mem / (cyc_p8 * 4.0)); + if (other_nukes < 0.0) other_nukes = 0.0; + + // Order : + // 0:resteers, 1:divider, 2:dram, 3:dsb, 4:dsb_switches, 5:few_uops, 6:fp_arith, 7:fused + // 8:icache, 9:itlb, 10:l1, 11:l2, 12:l3, 13:lcp, 14:mem_ops, 15:ms_uops, 16:mite + // 17:ms_swit, 18:non_fused_br, 19:other_light, 20:oth_misp, 21:nukes, 22:pmm, 23:ports, 24:serial, 25:store + + final[0] = resteers * 100.0; + final[1] = divider * 100.0; + final[2] = dram_bound * 100.0; + final[3] = dsb * 100.0; + final[4] = dsb2mite * 100.0; + final[5] = few_uops * 100.0; + final[6] = fp_arith * 100.0; + final[7] = fused * 100.0; + final[8] = icache * 100.0; + final[9] = itlb * 100.0; + final[10] = l1_bound * 100.0; + final[11] = l2_bound * 100.0; + final[12] = l3_bound * 100.0; + final[13] = lcp * 100.0; + final[14] = mem_ops * 100.0; + final[15] = ms_uops * 100.0; + final[16] = mite * 100.0; + final[17] = ms_swit * 100.0; + final[18] = non_fused * 100.0; + final[19] = other_light * 100.0; + final[20] = other_misp * 100.0; + final[21] = other_nukes * 100.0; + final[22] = 0.0; // pmm (Intel Optane DC, discontuated tech) + final[23] = ports_util * 100.0; + final[24] = serial * 100.0; + final[25] = store_bnd * 100.0; + + for (int i=0; i<26; i++) if (final[i] < 0.0) final[i] = 0.0; + } else { + for (int i=0; i<26; i++) final[i] = 0.0; + } +} + +// Modern Inter arch + +static const char *intel_modern_tma_l1_events[] = { + "@icl_slots", // 0x0400 (Group Leader) + "@icl_retiring", // 0x8000 + "@icl_bad_spec", // 0x8100 + "@icl_fe_bound", // 0x8200 + "@icl_be_bound" // 0x8300 +}; + +static void compute_intel_modern_tma_l1(const double *raw_values, double *final_results) { + double slots = raw_values[0]; + double retiring = raw_values[1]; + double bad_spec = raw_values[2]; + double fe_bound = raw_values[3]; + double be_bound = raw_values[4]; + + if (slots > 0) { + final_results[0] = (retiring / slots) * 100.0; + final_results[1] = (bad_spec / slots) * 100.0; + final_results[2] = (fe_bound / slots) * 100.0; + final_results[3] = (be_bound / slots) * 100.0; + } else { + final_results[0] = final_results[1] = final_results[2] = final_results[3] = 0.0; + } +} + +static const char *intel_modern_tma_l2_events[] = { + "@icl_slots", + "@icl_retiring", + "@icl_heavy_ops", + "@icl_bad_spec", + "@icl_br_mispredict", + "@icl_fe_bound", + "@icl_fetch_lat", + "@icl_be_bound", + "@icl_mem_bound" +}; + +static void compute_intel_modern_tma_l2(const double *raw, double *final) { + double slots = raw[0]; + + if (slots > 0) { + double retiring = raw[1]; + double heavy_ops = raw[2]; + double bad_spec = raw[3]; + double br_mispredict = raw[4]; + double fe_bound = raw[5]; + double fetch_lat = raw[6]; + double be_bound = raw[7]; + double mem_bound = raw[8]; + + // Retiring + final[0] = ((retiring - heavy_ops) / slots) * 100.0; // Light Ops + final[1] = (heavy_ops / slots) * 100.0; // Heavy Ops + + // Bad Speculation + final[2] = ((bad_spec - br_mispredict) / slots) * 100.0; // Machine Clears + final[3] = (br_mispredict / slots) * 100.0; // Branch Mispredicts + + // Frontend Bound + final[4] = ((fe_bound - fetch_lat) / slots) * 100.0; // Fetch Bandwidth + final[5] = (fetch_lat / slots) * 100.0; // Fetch Latency + + // Backend Bound + final[6] = ((be_bound - mem_bound) / slots) * 100.0; // Core Bound + final[7] = (mem_bound / slots) * 100.0; // Memory Bound + + for (int i = 0; i < 8; i++) { + if (final[i] < 0.0) + final[i] = 0.0; + } + } else { + for (int i = 0; i < 8; i++) final[i] = 0.0; + } +} + +static const int icl_l3_mem_passes[] = {4, 3}; + +static const char *intel_modern_tma_l3_mem_events[] = { + // Pass 1 + "@icl_cyc", + "@icl_stalls_mem_any", + "@icl_stalls_l1d_miss", + "@icl_stalls_l2_miss", + + // Pass 2 + "@icl_cyc", + "@icl_stalls_l3_miss", + "@icl_bound_on_stores" +}; + +// intel_modern_tma_l3_mem use the same compute function as skl + +static const int icl_l3_passes[] = {5, 5, 5, 5, 5, 5, 5, 5}; + +static const char *intel_modern_tma_l3_events[] = { + // Pass 1: Memory Bounds + "@icl_cyc", "@icl_stalls_mem_any", "@icl_stalls_l1d_miss", "@icl_stalls_l2_miss", "@icl_stalls_l3_miss", + // Pass 2: Execution Stalls + "@icl_cyc", "@icl_divider_active", "@icl_core_stalls", "@icl_bound_on_stores", "@icl_scoreboard", + // Pass 3: Frontend Latency + "@icl_cyc", "@icl_icache_miss", "@icl_itlb_miss", "@icl_clear_resteer", "@icl_lcp", + // Pass 4: Frontend Bandwidth + "@icl_cyc", "@icl_dsb2mite", "@icl_ms_switches", "@icl_idq_mite", "@icl_idq_dsb", + // Pass 5: Instruction Mix + "@icl_cyc", "@icl_idq_ms", "@icl_macro_fused", "@icl_mem_inst", "@icl_br_inst", + // Pass 6: Floating Point 1 + "@icl_cyc", "@icl_fp_scalar_s", "@icl_fp_scalar_d", "@icl_fp_128_s", "@icl_fp_128_d", + // Pass 7: Floating Point 2 + "@icl_cyc", "@icl_fp_256_s", "@icl_fp_256_d", "@icl_fp_512_s", "@icl_fp_512_d", + // Pass 8: Parent Nodes (Other) + "@icl_cyc", "@icl_retiring_uops", "@icl_issued_any", "@icl_br_misp", "@icl_nukes_mem" +}; + +// intel_modern_tma_l3 use the same compute function as skl + +// Zen arch + +static const char *amd_zen4_tma_l1_events[] = { + "@zen4_cyc", // 0x0076 (Cycles) + "@zen4_fe", // 0x1000001A0 (Dispatch slots empty because frontend is stalled) + "@zen4_disp", // 0x07AA (Ops dispatched from any source) + "@zen4_ret" // 0x00C1 (Ops retired) +}; + +static inline void compute_amd_tma_l1_generic(const double *raw_values, double *final_results, double pipeline_width) { + double cycles = raw_values[0]; + double fe_raw = raw_values[1]; + double disp_raw = raw_values[2]; + double ret_raw = raw_values[3]; + + double slots = pipeline_width * cycles; + + if (slots > 0) { + double r_fe_bound = fe_raw / slots; + + double r_bad_spec = (disp_raw - ret_raw) / slots; + //assert(r_bad_spec >= 0.0); + if (r_bad_spec < 0.0) r_bad_spec = 0.0; + double r_retiring = ret_raw / slots; + + double r_be_bound = 1.0 - (r_fe_bound + r_bad_spec + r_retiring); + //assert(r_be_bound >= 0.0); + if (r_be_bound < 0.0) r_be_bound = 0.0; + + final_results[0] = r_retiring * 100.0; + final_results[1] = r_bad_spec * 100.0; + final_results[2] = r_fe_bound * 100.0; + final_results[3] = r_be_bound * 100.0; + } else { + final_results[0] = final_results[1] = final_results[2] = final_results[3] = 0.0; + } +} + +static void compute_amd_zen34_tma_l1(const double *raw, double *final) { + compute_amd_tma_l1_generic(raw, final, 6.0); // Zen 3 / Zen 4 +} + +static void compute_amd_zen1_tma_l1(const double *raw, double *final) { + compute_amd_tma_l1_generic(raw, final, 4.0); // Zen 1 / Zen 2 +} + +static const int amd_zen4_l2_passes[] = {5, 4}; + +static const char *amd_zen4_tma_l2_events[] = { + "@zen4_cyc", + "@zen4_be_mem", // mem stall + "@zen4_be_cpu", // cpu stall + "@zen4_fe_lat", // fetch latency + "@zen4_fe_tot", // total frontend (no cmask) + + "@zen4_cyc", + "@zen4_bs_misp", // branch mispredict + "@zen4_bs_resync", // pipeline restart (machine clear) + "@zen4_ret_micro" // Heavy ops +}; + +static void compute_amd_zen4_tma_l2(const double *raw, double *final) { + double slots_p1 = raw[0] * 6.0; // pipeline width + double slots_p2 = raw[4] * 6.0; + + if (slots_p1 > 0 && slots_p2 > 0) { + // Backend Bound + double be_mem = raw[1] / slots_p1; + double be_cpu = raw[2] / slots_p1; + + // Frontend Bound (Bandwitdth = total - latency) + double fe_lat = raw[3] / slots_p1; + double fe_tot = raw[4] / slots_p1; + double fe_bw = fe_tot - fe_lat; + if (fe_bw < 0.0) fe_bw = 0.0; + + // Bad Speculation + double bs_misp = raw[6] / slots_p2; + double bs_clear = raw[7] / slots_p2; + + // Retiring + double ret_heavy = raw[8] / slots_p2; + + double total_retiring = 1.0 - (be_mem + be_cpu + fe_tot + bs_misp + bs_clear); + double ret_light = total_retiring - ret_heavy; + + // L2 : Light, Heavy, Clears, Mispredicts, FE Bandwidth, FE Latency, Core Bound, Memory Bound + final[0] = ret_light * 100.0; + final[1] = ret_heavy * 100.0; + final[2] = bs_clear * 100.0; + final[3] = bs_misp * 100.0; + final[4] = fe_bw * 100.0; + final[5] = fe_lat * 100.0; + final[6] = be_cpu * 100.0; + final[7] = be_mem * 100.0; + + for(int i=0; i<8; i++) if (final[i] < 0.0) final[i] = 0.0; + } else { + for(int i=0; i<8; i++) final[i] = 0.0; + } +} + + +// ARM arch + +static const int arm_l1_passes[] = {5}; +static const char *arm_tma_l1_events[] = { + "@arm_cyc", "@arm_fe", "@arm_be", "@arm_inst", "@arm_brmisp" +}; + +static void compute_arm_tma_l1(const double *raw, double *final) { + double cyc = raw[0]; + if (cyc > 0) { + double fe_bound = raw[1] / cyc; + double be_bound = raw[2] / cyc; + + // Cycle where at leat 1 instruction has been issued + double remaining = 1.0 - fe_bound - be_bound; + if (remaining < 0.0) remaining = 0.0; + + // Estimation : 10 cycles lost per branch miss + // TODO : check doc + // Cortex-A76 = 14 cycles (https://www.7-cpu.com/cpu/Cortex-A76.html) + // Cortex-A53 = 7 cycles (https://www.7-cpu.com/cpu/Cortex-A53.html) + // HPE RL300 ? + double bad_spec = (raw[4] * 10.0) / cyc; + if (bad_spec > remaining) bad_spec = remaining; + + double retiring = remaining - bad_spec; + + final[0] = retiring * 100.0; + final[1] = bad_spec * 100.0; + final[2] = fe_bound * 100.0; + final[3] = be_bound * 100.0; + } else { + final[0] = final[1] = final[2] = final[3] = 0.0; + } +} + +static const int arm_l2_passes[] = {5, 3}; +static const char *arm_tma_l2_events[] = { + // Pass 1 : Backend (Memory vs CPU) + "@arm_cyc", + "@arm_be", + "@arm_be_mem", + "@arm_be_cpu", + "@arm_fe", + + // Pass 2 : Bad Speculation + Frontend latency + "@arm_cyc", + "@arm_l1i_miss", + "@arm_brmisp" +}; + +static void compute_arm_tma_l2(const double *raw, double *final) { + double cyc1 = raw[0]; + double cyc2 = raw[5]; + + if (cyc1 > 0 && cyc2 > 0) { + // Backend Bound + double be_tot = raw[1]; + double be_mem = raw[2]; + double be_cpu = raw[3]; + + double r_be_mem = 0.0, r_be_cpu = 0.0; + double r_be_tot = be_tot / cyc1; + + // Backend normalisation + if (be_mem + be_cpu > 0) { + r_be_mem = r_be_tot * (be_mem / (be_mem + be_cpu)); + r_be_cpu = r_be_tot * (be_cpu / (be_mem + be_cpu)); + } else { + r_be_mem = r_be_tot; + } + + // Frontend Bound + double fe_tot = raw[4] / cyc1; + // Estimation : 15 cycles to get instruction from L2/RAM + // TODO : check doc + double fe_lat = (raw[6] * 15.0) / cyc2; + if (fe_lat > fe_tot) fe_lat = fe_tot; + double fe_bw = fe_tot - fe_lat; + + // Bad Speculation + Retiring + double remaining = 1.0 - fe_tot - r_be_tot; + if (remaining < 0.0) remaining = 0.0; + + double bad_spec = (raw[7] * 10.0) / cyc2; + if (bad_spec > remaining) bad_spec = remaining; + + double retiring = remaining - bad_spec; + + // Light, Heavy, Clears, Mispredicts, FE BW, FE Lat, Core Bound, Memory Bound + final[0] = retiring * 100.0; // Every instruction is Light + final[1] = 0.0; // No heavy ops + final[2] = 0.0; // Clear + final[3] = bad_spec * 100.0; // Mispredicts + final[4] = fe_bw * 100.0; // Frontend Bandwidth + final[5] = fe_lat * 100.0; // Frontend Latency + final[6] = r_be_cpu * 100.0; // Core bound + final[7] = r_be_mem * 100.0; // Memory bound + + for(int i=0; i<8; i++) if (final[i] < 0.0) final[i] = 0.0; + } else { + for(int i=0; i<8; i++) final[i] = 0.0; + } +} + + + +// Resolver logic + +typedef struct { + const char *metric_name; + cpu_uarch_t uarch; + + int num_results; + int num_passes; + int num_hw_events; + const int *events_per_pass; + const char **hw_events; + void (*compute_formula)(const double *raw_values, double *final_results); +} metric_registry_entry_t; + +static const int pass_1[] = {1}; +static const int pass_4[] = {4}; +static const int pass_5[] = {5}; +static const int pass_9[] = {9}; + +static const metric_registry_entry_t METRICS_REGISTRY[] = { + // Topdown L1 + {"TopdownL1", UARCH_INTEL_OLD, 4, 1, 5, pass_5, skl_tma_l1_events, compute_skl_tma_l1}, + {"TopdownL1", UARCH_INTEL_MODERN, 4, 1, 5, pass_5, intel_modern_tma_l1_events, compute_intel_modern_tma_l1}, + {"TopdownL1", UARCH_AMD_ZEN_4, 4, 1, 4, pass_4, amd_zen4_tma_l1_events, compute_amd_zen34_tma_l1}, + {"TopdownL1", UARCH_ARM, 4, 1, 5, arm_l1_passes, arm_tma_l1_events, compute_arm_tma_l1}, + + // Topdown L2 + {"TopdownL2", UARCH_INTEL_OLD, 8, 3, 12, skl_l2_passes, skl_tma_l2_events, compute_skl_tma_l2}, + {"TopdownL2", UARCH_INTEL_MODERN, 8, 1, 9, pass_9, intel_modern_tma_l2_events, compute_intel_modern_tma_l2}, + {"TopdownL2", UARCH_AMD_ZEN_4, 8, 2, 9, amd_zen4_l2_passes, amd_zen4_tma_l2_events, compute_amd_zen4_tma_l2}, + + // Topdown L3 + {"TopdownL3", UARCH_INTEL_OLD, 26, 8, 40, skl_l3_passes, skl_tma_l3_events, compute_skl_tma_l3}, + {"TopdownL3", UARCH_INTEL_MODERN, 26, 8, 40, icl_l3_passes, intel_modern_tma_l3_events, compute_skl_tma_l3}, + + // Topdown L3 Mem + {"TopdownL3_Mem", UARCH_INTEL_OLD, 5, 2, 7, skl_l3_mem_passes, skl_tma_l3_mem_events, compute_skl_tma_l3_mem}, + {"TopdownL3_Mem", UARCH_INTEL_MODERN, 5, 2, 7, icl_l3_mem_passes, intel_modern_tma_l3_mem_events, compute_skl_tma_l3_mem}, +}; +static const int METRICS_REGISTRY_SIZE = sizeof(METRICS_REGISTRY) / sizeof(METRICS_REGISTRY[0]); + +#endif //__linux__ + + +/* + * Tested TopdownL1 : + * - INTEL_SKYLAKE_CASCADE (skylake, kaby-lake)L1,L2 + * - INTEL_ICELAKE_SAPPHIRE (raptor lake) L1,L2 + * - AMD_ZEN_4 (Zen 4) L1,L2 + * + * Untested : + * - AMD_ZEN_1_2 + * + * Not implemented : + * - Aarch64 + * - Zen 3 + * - L3 support + * + * + */ +int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { +#ifndef __linux__ + return 0; +#else + cpu_uarch_t current_cpu = get_current_uarch(); + + for (int i = 0; i < METRICS_REGISTRY_SIZE; i++) { + const metric_registry_entry_t *entry = &METRICS_REGISTRY[i]; + + if (entry->uarch == current_cpu && strcmp(entry->metric_name, metric_name) == 0) { + + out_resolver->is_supported = 1; + out_resolver->num_results = entry->num_results; + out_resolver->num_passes = entry->num_passes; + out_resolver->num_hw_events = entry->num_hw_events; + out_resolver->events_per_pass = entry->events_per_pass; + out_resolver->hw_events = entry->hw_events; + out_resolver->compute_formula = entry->compute_formula; + + return 1; + } + } + + out_resolver->is_supported = 0; + return 0; +#endif +} + +int get_perf_metric_results_count(const char *metric_name) { + metric_resolver_t resolver; + if (resolve_metric(metric_name, &resolver) && resolver.is_supported) { + return resolver.num_results; + } + return 1; +} diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.h b/src/xtc/csrcs/runtimes/host/perf_metrics.h new file mode 100644 index 000000000..ea12c18f5 --- /dev/null +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.h @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2024-2026 The XTC Project Authors + */ +#ifndef _PERF_METRICS_H +#define _PERF_METRICS_H + +typedef struct { + int is_supported; + int num_results; + + int num_passes; + int num_hw_events; // Total events with all passes + const int *events_per_pass; // events per passes (ex: {4, 4, 4}) + const char **hw_events; // flat array with all events name + + void (*compute_formula)(const double *raw_values, double *final_results); +} metric_resolver_t; + +int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver); +int get_perf_metric_results_count(const char *metric_name); + + + +#endif diff --git a/src/xtc/itf/runtime/common.py b/src/xtc/itf/runtime/common.py index f42b4c76d..d56596c21 100644 --- a/src/xtc/itf/runtime/common.py +++ b/src/xtc/itf/runtime/common.py @@ -72,6 +72,7 @@ def evaluate( @abstractmethod def evaluate_perf( self, + results: Any, pmu_events: list[str], repeat: int, number: int, @@ -79,10 +80,11 @@ def evaluate_perf( cfunc: CFunc, args: Any, nargs: int, - ) -> list[float]: + ) -> None: """Evaluate a function with performance counter measurements. Args: + results: Pointer to array of doubles to store performance results. pmu_events: List of performance events to measure. repeat: Number of times to repeat the measurement. number: Number of function calls per repeat. diff --git a/src/xtc/runtimes/accelerator/gpu/GPUDevice.py b/src/xtc/runtimes/accelerator/gpu/GPUDevice.py index 2fc3a4e1b..1ca468c98 100644 --- a/src/xtc/runtimes/accelerator/gpu/GPUDevice.py +++ b/src/xtc/runtimes/accelerator/gpu/GPUDevice.py @@ -236,6 +236,7 @@ def evaluate( @override def evaluate_perf( self, + results: Any, pmu_events: list[str], repeat: int, number: int, @@ -243,14 +244,9 @@ def evaluate_perf( cfunc: CFunc, args: Any, nargs: int, - ) -> list[float]: - values_num = 1 - if len(pmu_events) > 0: - values_num = len(pmu_events) - # FIXME check if the PMU events are supported by the target - results_array = (ctypes.c_double * (repeat * values_num))() + ) -> None: self.__get_runtime_func("evaluate_perf")( - ctypes.cast(results_array, ctypes.POINTER(ctypes.c_double)), + ctypes.cast(results, ctypes.POINTER(ctypes.c_double)), ctypes.c_int(len(pmu_events)), _str_list_to_c(pmu_events), ctypes.c_int(repeat), @@ -260,7 +256,6 @@ def evaluate_perf( ctypes.cast(args, ctypes.POINTER(ctypes.c_voidp)), ctypes.c_int(nargs), ) - return [float(x) for x in results_array] @override def evaluate_packed( diff --git a/src/xtc/runtimes/accelerator/mppa/MppaDevice.py b/src/xtc/runtimes/accelerator/mppa/MppaDevice.py index 33a33801e..b8ee70279 100644 --- a/src/xtc/runtimes/accelerator/mppa/MppaDevice.py +++ b/src/xtc/runtimes/accelerator/mppa/MppaDevice.py @@ -656,6 +656,7 @@ def evaluate( @override def evaluate_perf( self, + results: Any, pmu_events: list[str], repeat: int, number: int, @@ -663,7 +664,7 @@ def evaluate_perf( cfunc: CFunc, args: Any, nargs: int, - ) -> list[float]: + ) -> None: if not self.mppa_initialized: self.init_device() assert self.lib_loader is not None @@ -688,6 +689,7 @@ def evaluate_perf( ctypes.c_int, ctypes.CFUNCTYPE(ctypes.c_voidp), ctypes.POINTER(ctypes.c_voidp), + ctypes.c_int, ] evaluate_perf_fn.restype = None evaluate_perf_fn( @@ -707,15 +709,13 @@ def evaluate_perf( mppa_pmu_events, repeat ) # Interleave the results of host and mppa events to match the requested pmu_events order - out = [] host_iter = iter(host_results) mppa_iter = iter(mppa_pmu_events_results) - for ev in pmu_events: + for i, ev in enumerate(pmu_events): if ev.startswith("mppa."): - out.append(next(mppa_iter)) + results[i] = next(mppa_iter) else: - out.append(next(host_iter)) - return out + results[i] = next(host_iter) @override def evaluate_packed( diff --git a/src/xtc/runtimes/host/HostRuntime.py b/src/xtc/runtimes/host/HostRuntime.py index 36ff54307..d13bb98d7 100644 --- a/src/xtc/runtimes/host/HostRuntime.py +++ b/src/xtc/runtimes/host/HostRuntime.py @@ -93,6 +93,7 @@ def evaluate( @override def evaluate_perf( self, + results: Any, pmu_events: list[str], repeat: int, number: int, @@ -100,14 +101,9 @@ def evaluate_perf( cfunc: CFunc, args: Any, nargs: int, - ) -> list[float]: - values_num = 1 - if len(pmu_events) > 0: - values_num = len(pmu_events) - # FIXME check if the PMU events are supported by the target - results_array = (ctypes.c_double * (repeat * values_num))() + ) -> None: self.__get_runtime_func("evaluate_perf")( - ctypes.cast(results_array, ctypes.POINTER(ctypes.c_double)), + ctypes.cast(results, ctypes.POINTER(ctypes.c_double)), ctypes.c_int(len(pmu_events)), _str_list_to_c(pmu_events), ctypes.c_int(repeat), @@ -117,7 +113,6 @@ def evaluate_perf( ctypes.cast(args, ctypes.POINTER(ctypes.c_voidp)), ctypes.c_int(nargs), ) - return [float(x) for x in results_array] @override def evaluate_packed( diff --git a/src/xtc/runtimes/host/runtime.py b/src/xtc/runtimes/host/runtime.py index 4f10bf02d..f6e2b70c5 100644 --- a/src/xtc/runtimes/host/runtime.py +++ b/src/xtc/runtimes/host/runtime.py @@ -156,6 +156,7 @@ def _compile_runtime(out_dll: str, tdir: str, runtime_type: RuntimeType): debug_opts = "-DRUNTIME_DEBUG=1" if RUNTIME_DEBUG else "" files = [ "evaluate_perf.c", + "perf_metrics.c", "cndarray.c", "alloc.c", "fclock.c", @@ -175,7 +176,7 @@ def _compile_runtime(out_dll: str, tdir: str, runtime_type: RuntimeType): obj_files = [f"{tdir}/{Path(file).stem}.o" for file in src_files] for i, file in enumerate(src_files): cmd = ( - "cc -c -O2 -march=native -fPIC " + "cc -c -O2 -g -march=native -fPIC " f"-I{src_dir} {debug_opts} {pfm_opts} {gpu_opts} -I{src_dir}/../accelerator/gpu " f"-o {obj_files[i]} {file}" ) diff --git a/src/xtc/schedules/ttile/scheme_to_xtc.py b/src/xtc/schedules/ttile/scheme_to_xtc.py index 96a2c3bbf..8c6632382 100644 --- a/src/xtc/schedules/ttile/scheme_to_xtc.py +++ b/src/xtc/schedules/ttile/scheme_to_xtc.py @@ -736,8 +736,8 @@ def build_schedule_from_ttile( # Launch scheme execution & measurement through xdsl-transform script (higher level) -# - By default, if pmu_counters is "[]", the time (+ the peak_perf) is reported -# - peak_perf is computed from the first "time" or "clk" counter detected inside "pmu_counters" +# - By default, if hw_counters is "[]", the time (+ the peak_perf) is reported +# - peak_perf is computed from the first "time" or "clk" counter detected inside "hw_counters" # - l_verbose: (print_source_ir, print_transformed_ir, print_assembly) def launch_and_measure_scheme_graph_interf( comp: Computation, @@ -745,7 +745,7 @@ def launch_and_measure_scheme_graph_interf( scheme: List[Atom], dsizes: dict[str, int], backend: str, - pmu_counters: list[str] = [], + hw_counters: list[str] = [], l_verbose: tuple[int, int, int] = (False, False, False), ) -> dict[str, float]: # 1) Computation - described as a graph @@ -845,19 +845,19 @@ def launch_and_measure_scheme_graph_interf( module = compiler.compile(sched) evaluator = module.get_evaluator( validate=True, - pmu_counters=pmu_counters, + hw_counters=hw_counters, **evaluate_args, ) results, code, error = evaluator.evaluate() - # If we did not have any pmu_counters specified, the only returned value is "time" - if pmu_counters == []: - pmu_counters = ["time"] + # If we did not have any hw_counters specified, the only returned value is "time" + if hw_counters == []: + hw_counters = ["time"] - assert len(results) == len(pmu_counters) + assert len(results) == len(hw_counters) res_measurement = dict() - for i in range(len(pmu_counters)): - res_measurement[pmu_counters[i]] = float(results[i]) + for i in range(len(hw_counters)): + res_measurement[hw_counters[i]] = float(results[i]) # Peak_perf computation: # - We detect if we have a time/cycle counter in res_measurement @@ -866,18 +866,18 @@ def launch_and_measure_scheme_graph_interf( ltime_cycles_counter_names = ltime_counter_names + lcycles_counter_names i_time_ref = -1 - for i in range(len(pmu_counters)): - if pmu_counters[i] in ltime_cycles_counter_names: + for i in range(len(hw_counters)): + if hw_counters[i] in ltime_cycles_counter_names: i_time_ref = i break # One of the counter is time or num_cycle => compute the peak_perf from it if i_time_ref >= 0: - if pmu_counters[i_time_ref] in ltime_counter_names: # If the counter is time - time = res_measurement[pmu_counters[i_time_ref]] + if hw_counters[i_time_ref] in ltime_counter_names: # If the counter is time + time = res_measurement[hw_counters[i_time_ref]] if ( - pmu_counters[i_time_ref] != "time" + hw_counters[i_time_ref] != "time" ): # "time" is in second, the rest in "ns" time = time / 1e9 @@ -907,9 +907,9 @@ def launch_and_measure_scheme_graph_interf( ) elif ( - pmu_counters[i_time_ref] in lcycles_counter_names + hw_counters[i_time_ref] in lcycles_counter_names ): # If the counter is a num_cycle - cycles = res_measurement[pmu_counters[i_time_ref]] + cycles = res_measurement[hw_counters[i_time_ref]] num_ops = compute_number_ops(comp, dsizes) peak_cycles = cpu_peak_cycle(num_ops, dtype) diff --git a/src/xtc/targets/accelerator/gpu/GPUEvaluator.py b/src/xtc/targets/accelerator/gpu/GPUEvaluator.py index 7da994e54..55f545df0 100644 --- a/src/xtc/targets/accelerator/gpu/GPUEvaluator.py +++ b/src/xtc/targets/accelerator/gpu/GPUEvaluator.py @@ -40,7 +40,7 @@ def __init__(self, module: "gpu.GPUModule", **kwargs: Any) -> None: self._reference_impl = kwargs.get( "reference_impl", self._module._reference_impl ) - self._pmu_counters = kwargs.get("pmu_counters", []) + self._hw_counters = kwargs.get("hw_counters", []) assert self._module.file_type == "shlib", "only support shlib for evaluation" @@ -87,7 +87,7 @@ def evaluate(self) -> tuple[list[float], int, str]: results = evaluate_performance( func, parameters, - self._pmu_counters, + self._hw_counters, self._repeat, self._number, self._min_repeat_ms, diff --git a/src/xtc/targets/accelerator/mppa/MppaEvaluator.py b/src/xtc/targets/accelerator/mppa/MppaEvaluator.py index 27fd8594b..47cca8f90 100644 --- a/src/xtc/targets/accelerator/mppa/MppaEvaluator.py +++ b/src/xtc/targets/accelerator/mppa/MppaEvaluator.py @@ -44,7 +44,7 @@ def __init__(self, module: "mppa.MppaModule", **kwargs: Any) -> None: self._reference_impl = kwargs.get( "reference_impl", self._module._reference_impl ) - self._pmu_counters = kwargs.get("pmu_counters", []) + self._hw_counters = kwargs.get("hw_counters", []) assert self._module.file_type == "shlib", "only support shlib for evaluation" @@ -78,7 +78,7 @@ def evaluate(self) -> tuple[list[float], int, str]: results = evaluate_performance( func, parameters, - self._pmu_counters, + self._hw_counters, self._repeat, self._number, self._min_repeat_ms, diff --git a/src/xtc/targets/host/HostEvaluator.py b/src/xtc/targets/host/HostEvaluator.py index a04ae4280..d9db38cdf 100644 --- a/src/xtc/targets/host/HostEvaluator.py +++ b/src/xtc/targets/host/HostEvaluator.py @@ -43,7 +43,7 @@ def __init__(self, module: "host.HostModule", **kwargs: Any) -> None: self._reference_impl = kwargs.get( "reference_impl", self._module._reference_impl ) - self._pmu_counters = kwargs.get("pmu_counters", []) + self._hw_counters = kwargs.get("hw_counters", []) self._runtime = kwargs.get("runtime", HostRuntime()) assert self._module.file_type == "shlib", "only support shlib for evaluation" @@ -77,7 +77,7 @@ def evaluate(self) -> tuple[list[float], int, str]: results = evaluate_performance( func, parameters, - self._pmu_counters, + self._hw_counters, self._repeat, self._number, self._min_repeat_ms, diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index d0fceb6f4..db2936fa3 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -2,18 +2,25 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024-2026 The XTC Project Authors # -from typing import Any, Callable, cast -from xtc.itf.graph import Graph import ctypes +import os +import shutil +import signal +import subprocess +import time +from typing import Any, Callable, cast + import numpy as np -from xtc.utils.numpy import np_init -from xtc.runtimes.types.ndarray import NDArray -from xtc.graphs.xtc.graph import XTCGraph -from xtc.graphs.xtc.expr import XTCTensorExpr + from xtc.graphs.xtc.data import XTCTensor -from xtc.utils.cfunc import CFunc, CArgValue, CArgCode +from xtc.graphs.xtc.expr import XTCTensorExpr +from xtc.graphs.xtc.graph import XTCGraph +from xtc.itf.graph import Graph from xtc.itf.runtime.common import CommonRuntimeInterface from xtc.runtimes.host.HostRuntime import HostRuntime +from xtc.runtimes.types.ndarray import NDArray +from xtc.utils.cfunc import CArgCode, CArgValue, CFunc +from xtc.utils.numpy import np_init __all__: list[str] = [] @@ -145,10 +152,93 @@ def validate_outputs( return ([], 0, "") +# skylake +# perf list --no-desc | grep -i -E -A 59 tma_L[1-4]_group: +DERIVED_METRICS_SIZES = { + "TopdownL1": 4, # tma_backend_bound, tma_bad_speculation, tma_frontend_bound, tma_info_core_coreipc, tma_info_inst_mix_instructions, tma_info_thread_slots, tma_retiring + "TopdownL2": 8, # tma_branch_mispredicts, tma_core_bound, tma_fetch_bandwidth, tma_fetch_latency, tma_heavy_operations, tma_light_operations, tma_machine_clears, tma_memory_bound + "TopdownL3": 26, # tma_branch_resteers, tma_divider, tma_dram_bound, tma_dsb, tma_dsb_switches, tma_few_uops_instructions, tma_fp_arith, tma_fused_instructions, tma_icache_misses, tma_itlb_misses, tma_l1_bound, tma_l2_bound, tma_l3_bound, tma_lcp, tma_memory_operations, tma_microcode_sequencer, tma_mite, tma_ms_switches, tma_non_fused_branches, tma_other_light_ops, tma_other_mispredicts, tma_other_nukes, tma_pmm_bound, tma_ports_utilization, tma_serializing_operation, tma_store_bound + "TopdownL3_Mem": 5, + "TopdownL4": 32, # tma_4k_aliasing, tma_assists, tma_cisc, tma_clears_resteers, tma_contested_accesses, tma_data_sharing, tma_decoder0_alone, tma_dtlb_load, tma_dtlb_store, tma_false_sharing, tma_fb_full, tma_fp_scalar, tma_fp_vector, tma_l1_hit_latency, tma_l3_hit_latency, tma_lock_latency, tma_mem_bandwidth, tma_mem_latency, tma_mispredicts_resteers, tma_nop_instructions, tma_ports_utilized_0, tma_ports_utilized_1, tma_ports_utilized_2, tma_ports_utilized_3m, tma_slow_pause, tma_split_loads, tma_split_stores, tma_sq_full, tma_store_fwd_blk, tma_store_latency, tma_unknown_branches, tma_x87_use + "TopdownL5": 15, # tma_alu_op_utilization, tma_fp_assists, tma_fp_vector_128b, tma_fp_vector_256b, tma_fp_vector_512b, tma_load_op_utilization, tma_load_stlb_hit, tma_load_stlb_miss, tma_local_mem, tma_mixing_vectors, tma_remote_cache, tma_remote_mem, tma_store_op_utilization, tma_store_stlb_hit, tma_store_stlb_miss + "TopdownL6": 8, # tma_port_0, tma_port_1, tma_port_2, tma_port_3, tma_port_4, tma_port_5, tma_port_6, tma_port_7 + # AMD specific + "backend_bound_memory": 1, + "backend_bound_cpu": 1, + "frontend_bound_latency": 1, + "frontend_bound_bandwidth": 1, +} + + +def _fallback_perf_stat( + failed_counters: list[str], run_dummy_workload: Callable[[], None] +) -> str: + perf_path = shutil.which("perf") + if not perf_path: + return "perf tool not found in PATH" + + my_pid = str(os.getpid()) + + perf_metrics = [c for c in failed_counters if c in DERIVED_METRICS_SIZES] + perf_events = [c for c in failed_counters if c not in DERIVED_METRICS_SIZES] + + # is_amd = False + # if sys.platform == "linux": + # try: + # with open("/proc/cpuinfo", "r") as f: + # if "AuthenticAMD" in f.read(): + # is_amd = True + # except Exception: + # pass + + # if is_amd and "TopdownL2" in perf_metrics: + # perf_metrics.remove("TopdownL2") + # perf_metrics.extend([ + # "backend_bound_memory", + # "backend_bound_cpu", + # "frontend_bound_latency", + # "frontend_bound_bandwidth", + # ]) + + cmd = [perf_path, "stat", "-p", my_pid] + + if perf_events: + cmd.extend(["-e", ",".join(perf_events)]) + if perf_metrics: + cmd.extend(["-M", ",".join(perf_metrics)]) + + try: + # print("[DEBUG] Starting perf...")q + perf_proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + # Time to hook to the process + time.sleep(0.75) + + run_dummy_workload() + + perf_proc.send_signal(signal.SIGINT) + _, stderr_output = perf_proc.communicate(timeout=5.0) + + cmd_str = " ".join(cmd) + formatted_fallback_output = f"$ {cmd_str}\n\n{stderr_output}" + + print(f"\n====== Fallback 'perf stat' Output for {failed_counters} ======\n") + print(stderr_output) + print("===================================\n") + + return formatted_fallback_output + + except Exception as e: + # print(f"[DEBUG] Fallback perf stat failed : {e}") + return f"Fallback perf stat failed: {e}" + + def evaluate_performance( func: Callable[[Any], Any], parameters: tuple[list[NDArray], list[NDArray]], - pmu_counters: list[str], + hw_counters: list[str], repeat: int, number: int, min_repeat_ms: int, @@ -157,11 +247,21 @@ def evaluate_performance( # TODO migrate host runtime to CommonRuntimeInterface cfunc = CFunc(func) args_tuples = cfunc.args_tuples([*parameters[0], *parameters[1]]) - values_num = 1 - if len(pmu_counters) > 0: - values_num = len(pmu_counters) - # FIXME check if the PMU counters are supported by the target + + if len(hw_counters) > 0: + values_num = 0 + for counter in hw_counters: + values_num += DERIVED_METRICS_SIZES.get(counter, 1) + # FIXME check if the HW counters are supported by the target + else: + values_num = 1 + # print(f"[DEBUG] values_num : {values_num}") results_array = (ctypes.c_double * (repeat * values_num))() + + args_array_packed = None + args_codes_packed = None + args_array = None + if cfunc.is_packed: args_array_packed = (CArgValue * len(args_tuples))( *[arg[0] for arg in args_tuples] @@ -171,7 +271,7 @@ def evaluate_performance( ) runtime.evaluate_packed_perf( results_array, - pmu_counters, + hw_counters, repeat, number, min_repeat_ms, @@ -180,13 +280,13 @@ def evaluate_performance( args_codes_packed, len(args_tuples), ) - eval_results = [float(x) for x in results_array] else: args_array = (ctypes.c_voidp * len(args_tuples))( *[arg[0] for arg in args_tuples] ) - eval_results = runtime.evaluate_perf( - pmu_counters, + runtime.evaluate_perf( + results_array, + hw_counters, repeat, number, min_repeat_ms, @@ -194,7 +294,60 @@ def evaluate_performance( args_array, len(args_array), ) - return (eval_results, 0, "") + # print(f"[DEBUG] results: {[round(x, 2) for x in results_array]}") + eval_results = [float(x) for x in results_array] + + failed_counters = [] + current_idx = 0 + + for counter in hw_counters: + size = DERIVED_METRICS_SIZES.get(counter, 1) + chunk = eval_results[current_idx : current_idx + size] + + if any(x == -1.0 for x in chunk) or all(x == 0.0 for x in chunk): + failed_counters.append(counter) + + current_idx += size + + # Fallback on linux perf tool + fallback_output = "" + if failed_counters: + print( + f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'..." + ) + + def dummy_workload(): + dummy_results = (ctypes.c_double * repeat)() + if cfunc.is_packed: + runtime.evaluate_packed_perf( + dummy_results, + [], + repeat, + number, + min_repeat_ms, + cfunc, + args_array_packed, + args_codes_packed, + len(args_tuples), + ) + else: + _args_array = (ctypes.c_voidp * len(args_tuples))( + *[arg[0] for arg in args_tuples] + ) + runtime.evaluate_perf( + dummy_results, + [], + repeat, + number, + min_repeat_ms, + cfunc, + args_array, + len(args_tuples), + ) + + fallback_output = _fallback_perf_stat(failed_counters, dummy_workload) + + return (eval_results, 0, fallback_output) def copy_outputs( diff --git a/tests/filecheck/evaluation/test_matmul_pmu_counters.py b/tests/filecheck/evaluation/test_matmul_pmu_counters.py index 51c7e5d23..fd55b76e3 100644 --- a/tests/filecheck/evaluation/test_matmul_pmu_counters.py +++ b/tests/filecheck/evaluation/test_matmul_pmu_counters.py @@ -53,7 +53,7 @@ evaluator = module.get_evaluator( validate=True, - pmu_counters=pmu_counters, + hw_counters=pmu_counters, ) results, code, error = evaluator.evaluate() print(f"CODE: {code}") diff --git a/tests/filecheck/evaluation/test_matmul_pmu_counters_gpu.py b/tests/filecheck/evaluation/test_matmul_pmu_counters_gpu.py index ab671deab..c6e045254 100644 --- a/tests/filecheck/evaluation/test_matmul_pmu_counters_gpu.py +++ b/tests/filecheck/evaluation/test_matmul_pmu_counters_gpu.py @@ -48,7 +48,7 @@ ] evaluator = module.get_evaluator( validate=True, - pmu_counters=pmu_counters, + hw_counters=pmu_counters, ) results, code, error = evaluator.evaluate() print(f"CODE: {code}") diff --git a/tests/filecheck/evaluation/test_matmul_pmu_counters_mppa.py b/tests/filecheck/evaluation/test_matmul_pmu_counters_mppa.py index 66a7376c1..c71de8e1c 100644 --- a/tests/filecheck/evaluation/test_matmul_pmu_counters_mppa.py +++ b/tests/filecheck/evaluation/test_matmul_pmu_counters_mppa.py @@ -42,7 +42,7 @@ evaluator = module.get_evaluator( validate=True, - pmu_counters=pmu_counters, + hw_counters=pmu_counters, ) results, code, error = evaluator.evaluate() print(f"CODE: {code}") diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py new file mode 100644 index 000000000..b6b2c072f --- /dev/null +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -0,0 +1,76 @@ +# RUN: python %s 2>&1 | filecheck %s +# UNSUPPORTED: mlir-target=nvgpu + +import xtc.graphs.xtc.op as O +from xtc.backends.mlir import Backend +from xtc.schedules.descript import descript_scheduler + +import sys + +#I, J, K, dtype = 4, 32, 512, "float32" # small +I, J, K, dtype = 1024, 2048, 4096, "float32" # medium +#I, J, K, dtype = 4096, 8192, 16384, "float32" # large + +a = O.tensor((I, K), dtype, name="A") +b = O.tensor((K, J), dtype, name="B") + +with O.graph(name="matmul") as gb: + O.matmul(a, b, name="C") + +graph = gb.graph + +impl = Backend(graph) + +# Schedule specification +schedule_spec = { + "i": {}, + "k": {}, + "j": {}, + f"i#{16}": {"unroll": 8}, + f"j#{16}": {"vectorize": True} +} + +# Compile +scheduler = impl.get_scheduler() +descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + spec=schedule_spec +) +sched = scheduler.schedule() + +compiler = impl.get_compiler( + dump_file="matmul_mlir", + shared_lib=True, + print_source_ir=False, + print_transformed_ir=False, + print_assembly=False +) +module = compiler.compile(sched) + +hw_counters = [] + +# Linux Perf counters +if sys.platform == "linux": + hw_counters += [ + "TopdownL1","TopdownL2", "TopdownL3" + ] +elif sys.platform == "darwin": + # On MacOS, requires sudo to get counters + # TODO: should be tested ideally + hw_counters = [] + + +evaluator = module.get_evaluator( + validate=True, + hw_counters=hw_counters, +) +results, code, error = evaluator.evaluate() +print(f"CODE: {code}") +print(f"{'counters'}: {hw_counters}") +print(f"{'results'}: {[round(x, 2) for x in results]}") + +# CHECK: CODE: 0 +# CHECK-NEXT: counters: +# CHECK-NEXT: results: diff --git a/tests/pytest/ttile/test_scheme_to_xtc.py b/tests/pytest/ttile/test_scheme_to_xtc.py index 455ae21f2..82786b29d 100644 --- a/tests/pytest/ttile/test_scheme_to_xtc.py +++ b/tests/pytest/ttile/test_scheme_to_xtc.py @@ -352,7 +352,7 @@ def test_launch_and_measure_scheme_graph_interf_pmu_counters(): backend = "tvm" res = launch_and_measure_scheme_graph_interf(comp, machine, scheme, dsizes, backend, - pmu_counters=["cycles", "l1d.replacement"]) #, l_verbose=[False,False,True]) + hw_counters=["cycles", "l1d.replacement"]) #, l_verbose=[False,False,True]) assert("cycles" in res.keys()) assert("l1d.replacement" in res.keys())