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())