From 464d2513ca071096990a6376005699d466794f33 Mon Sep 17 00:00:00 2001 From: Christophe Guillon Date: Fri, 12 Jun 2026 12:05:10 +0200 Subject: [PATCH 01/92] display: add --alpha/--diag options --- src/xtc/cli/display_results.py | 40 ++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/xtc/cli/display_results.py b/src/xtc/cli/display_results.py index 16bf1cf5..d8a8d045 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" ) From 59063b28d10450cccd1d5d1c408440270839ac47 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 21 May 2026 11:26:02 +0200 Subject: [PATCH 02/92] [TMA]Support tma TopdownL1 for older intel than Ice lake --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 124 +++++--- .../csrcs/runtimes/host/perf_event_linux.c | 42 ++- src/xtc/csrcs/runtimes/host/perf_metrics.c | 279 ++++++++++++++++++ src/xtc/csrcs/runtimes/host/perf_metrics.h | 22 ++ src/xtc/csrcs/runtimes/host/perf_topdown.h | 12 + .../csrcs/runtimes/host/perf_topdown_x86_64.c | 231 +++++++++++++++ src/xtc/itf/runtime/common.py | 4 +- src/xtc/runtimes/accelerator/gpu/GPUDevice.py | 11 +- src/xtc/runtimes/host/HostRuntime.py | 11 +- src/xtc/runtimes/host/runtime.py | 4 +- src/xtc/utils/evaluation.py | 20 +- .../evaluation/test_matmul_tma_counters.py | 58 ++++ 12 files changed, 748 insertions(+), 70 deletions(-) create mode 100644 src/xtc/csrcs/runtimes/host/perf_metrics.c create mode 100644 src/xtc/csrcs/runtimes/host/perf_metrics.h create mode 100644 src/xtc/csrcs/runtimes/host/perf_topdown.h create mode 100644 src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c create mode 100644 tests/filecheck/evaluation/test_matmul_tma_counters.py diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index fe64d1af..e2f705f5 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 */ @@ -188,45 +189,83 @@ 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) { + + metric_resolver_t resolver; + int is_derived_metric = 0; + + int hw_events_num = events_num; + const char **hw_events_names = (const char **)events_names; + double *hw_results = results; + + // Todo handle multiples tma from user + if (events_num == 1 && resolve_metric(events_names[0], &resolver)) { + if (!resolver.is_supported) { + fprintf(stderr, + "Error : The metric '%s' isn ot supported on this CPU.\n", + events_names[0]); + return; + } + + is_derived_metric = 1; + hw_events_num = resolver.num_hw_events; + hw_events_names = resolver.hw_events; + + hw_results = (double *)malloc(repeat * hw_events_num * sizeof(double)); + } + 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; + case 0: + evaluate0_perf(hw_results, hw_events_num, hw_events_names, + repeat, number, min_repeat_ms, + (func0_t)func); + break; + case 1: + evaluate1_perf(hw_results, hw_events_num, hw_events_names, + repeat, number, min_repeat_ms, + (func1_t)func, args[0]); + break; + case 2: + evaluate2_perf(hw_results, hw_events_num, hw_events_names, + repeat, number, min_repeat_ms, + (func2_t)func, args[0], args[1]); + break; + case 3: + evaluate3_perf(hw_results, hw_events_num, hw_events_names, + repeat, number, min_repeat_ms, + (func3_t)func, args[0], args[1], args[2]); + break; + case 4: + evaluate4_perf(hw_results, hw_events_num, hw_events_names, + repeat, number, min_repeat_ms, + (func4_t)func, args[0], args[1], args[2], args[3]); + break; + case 5: + evaluate5_perf(hw_results, hw_events_num, hw_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(hw_results, hw_events_num, hw_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; + } + if (is_derived_metric) { + for (int r = 0; r < repeat; r++) { + const double *run_raw_values = &hw_results[r * hw_events_num]; + double *run_final_results = &results[r * resolver.num_results]; + + printf("[DEBUG C] Run %d - Raw hardware counters per call:\n", r); + for (int e = 0; e < hw_events_num; e++) { + printf(" -> %-20s : %f\n", hw_events_names[e], run_raw_values[e]); + } + + resolver.compute_formula(run_raw_values, run_final_results); + } + free(hw_results); } } @@ -246,3 +285,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_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 3d28d0f8..f5b79003 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); @@ -215,13 +222,27 @@ int get_perf_event_config(const char *name, perf_event_args_t *event) { return 0; } } - + + if (strncmp(name, "@skl_", 5) == 0) { + event->mode = PERF_ARG_GENERIC; + event->args.config_pair.type = PERF_TYPE_RAW; + + if (strcmp(name, "@skl_slots") == 0) event->args.config_pair.event = 0x003c; + else if (strcmp(name, "@skl_fe_bound") == 0) event->args.config_pair.event = 0x019c; + else if (strcmp(name, "@skl_issued") == 0) event->args.config_pair.event = 0x010e; + else if (strcmp(name, "@skl_retiring") == 0) event->args.config_pair.event = 0x02c2; + else if (strcmp(name, "@skl_recovery") == 0) event->args.config_pair.event = 0x0100019d; + else return 1; + + return 0; +} + #if HAS_GPU if (strncmp(name, "gpu.", 4) == 0) { return get_perf_event_config__gpu(name, event); } #endif /* HAS_GPU */ - + #if HAS_PFM struct perf_event_attr *attr = malloc(sizeof(struct perf_event_attr)); init_perf_event_attr(attr); @@ -239,6 +260,9 @@ int get_perf_event_config(const char *name, perf_event_args_t *event) { event->mode = PERF_ARG_PTR; event->args.config_ptr = (const void *)arg; return 0; + } else { + fprintf(stderr, "[DEBUG] libpfm4 unknow event '%s' : %s\n", name, + pfm_strerror(ret)); } free(arg); free(attr); @@ -253,6 +277,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 00000000..1da67e3c --- /dev/null +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -0,0 +1,279 @@ +#include +#include +#include +#include +#include +#include + +#include "perf_metrics.h" + + +#if defined(__x86_64__) || defined(__i386__) + #define ARCH_IS_X86 1 + #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 + + +#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. */ + + + + +int detect_if_intel(void) { +#if ARCH_IS_X86 + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + if (__get_cpuid(0, &eax, &ebx, &ecx, &edx)) { + // ebx = "Genu" (0x756e6547) + // edx = "ineI" (0x49656e69) + // ecx = "ntel" (0x6c65746e) + if (ebx == 0x756e6547 && edx == 0x49656e69 && ecx == 0x6c65746e) { + return 1; + } + } +#endif + return 0; +} + +int detect_if_amd(void) { +#if ARCH_IS_X86 + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + + if (__get_cpuid(0, &eax, &ebx, &ecx, &edx)) { + // ebx = "Auth" (0x68747541) + // edx = "enti" (0x69746e65) + // ecx = "cAMD" (0x444d4163) + if (ebx == 0x68747541 && edx == 0x69746e65 && ecx == 0x444d4163) { + return 1; + } + } +#endif + return 0; +} + +int detect_if_arm(void) { + return ARCH_IS_ARM; +} + +#include +#include +static void compute_test_tma_doc(){ + uint64_t slots_a = read_slots(); + uint64_t metric_a = read_metrics(); + + { + srand(time(NULL)); + for(int i = 0; i<100000;i++){ + asm("":::"memory"); + volatile int a = rand(); + asm("":::"memory"); + + } + } + + uint64_t slots_b = read_slots(); + uint64_t metric_b = read_metrics(); + + // compute scaled metrics for measurement a + uint64_t retiring_slots_a = GET_METRIC(metric_a, 0) * slots_a; + uint64_t bad_spec_slots_a = GET_METRIC(metric_a, 1) * slots_a; + uint64_t fe_bound_slots_a = GET_METRIC(metric_a, 2) * slots_a; + uint64_t be_bound_slots_a = GET_METRIC(metric_a, 3) * slots_a; + + // compute delta scaled metrics between b and a + uint64_t retiring_slots = GET_METRIC(metric_b, 0) * slots_b - retiring_slots_a; + uint64_t bad_spec_slots = GET_METRIC(metric_b, 1) * slots_b - bad_spec_slots_a; + uint64_t fe_bound_slots = GET_METRIC(metric_b, 2) * slots_b - fe_bound_slots_a; + uint64_t be_bound_slots = GET_METRIC(metric_b, 3) * slots_b - be_bound_slots_a; + + uint64_t slots_delta = slots_b - slots_a; + float retiring_ratio = (float)retiring_slots / slots_delta; + float bad_spec_ratio = (float)bad_spec_slots / slots_delta; + float fe_bound_ratio = (float)fe_bound_slots / slots_delta; + float be_bound_ratio = (float)be_bound_slots / slots_delta; + + printf("Retiring %.2f%% Bad Speculation %.2f%% FE Bound %.2f%% BE Bound %.2f%%\n", + retiring_ratio * 100., + bad_spec_ratio * 100., + fe_bound_ratio * 100., + be_bound_ratio * 100.); + + /* + The individual ratios of L2 metric events for the measurement period can be + recreated from L1 and L2 metric counters. (Available on Sapphire Rapids and + later platforms) + */ + + // compute scaled metrics for measurement a + uint64_t heavy_ops_slots_a = GET_METRIC(metric_a, 4) * slots_a; + uint64_t br_mispredict_slots_a = GET_METRIC(metric_a, 5) * slots_a; + uint64_t fetch_lat_slots_a = GET_METRIC(metric_a, 6) * slots_a; + uint64_t mem_bound_slots_a = GET_METRIC(metric_a, 7) * slots_a; + + // compute delta scaled metrics between b and a + uint64_t heavy_ops_slots = GET_METRIC(metric_b, 4) * slots_b - heavy_ops_slots_a; + uint64_t br_mispredict_slots = GET_METRIC(metric_b, 5) * slots_b - br_mispredict_slots_a; + uint64_t fetch_lat_slots = GET_METRIC(metric_b, 6) * slots_b - fetch_lat_slots_a; + uint64_t mem_bound_slots = GET_METRIC(metric_b, 7) * slots_b - mem_bound_slots_a; + + slots_delta = slots_b - slots_a; + float heavy_ops_ratio = (float)heavy_ops_slots / slots_delta; + float light_ops_ratio = retiring_ratio - heavy_ops_ratio; + + float br_mispredict_ratio = (float)br_mispredict_slots / slots_delta; + float machine_clears_ratio = bad_spec_ratio - br_mispredict_ratio; + + float fetch_lat_ratio = (float)fetch_lat_slots / slots_delta; + float fetch_bw_ratio = fe_bound_ratio - fetch_lat_ratio; + + float mem_bound_ratio = (float)mem_bound_slots / slots_delta; + float core_bound_ratio = be_bound_ratio - mem_bound_ratio; + + printf("Heavy Operations %.2f%% Light Operations %.2f%% " + "Branch Mispredict %.2f%% Machine Clears %.2f%% " + "Fetch Latency %.2f%% Fetch Bandwidth %.2f%% " + "Mem Bound %.2f%% Core Bound %.2f%%\n", + heavy_ops_ratio * 100., + light_ops_ratio * 100., + br_mispredict_ratio * 100., + machine_clears_ratio * 100., + fetch_lat_ratio * 100., + fetch_bw_ratio * 100., + mem_bound_ratio * 100., + core_bound_ratio * 100.); + +} + +#include "perf_topdown.h" +static void compute_test_tma_guil(){ + perf_topdown_init(); + perf_topdown_start(); + + { + srand(time(NULL)); + for(int i = 0; i<100000;i++){ + asm("":::"memory"); + volatile int a = rand(); + asm("":::"memory"); + + } + } + + perf_topdown_stop(); + perf_topdown_dump(fdopen(1, "w")); + perf_topdown_fini(); +} + +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]; // recovery + + 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; + } +} + +int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { + //compute_test_tma_guil(); + if (strcmp(metric_name, "TopdownL1") == 0) { + int is_intel = detect_if_intel(); + int is_amd = detect_if_amd(); + int is_arm = detect_if_arm(); + assert(is_intel + is_amd + is_arm == 1); + + if (is_intel) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 5; + out_resolver->hw_events = skl_tma_l1_events; + out_resolver->num_results = 4; + out_resolver->compute_formula = compute_skl_tma_l1; + return 1; + } + else if (is_amd) { + // todo + return 0; + } + else if (is_arm) { + // todo + return 0; + } + + // Fallback : call perf stat in subprocess ? + return 0; // Unsuported hardware + } + + return 0; // Unknow metric +} + +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 00000000..f12754ce --- /dev/null +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.h @@ -0,0 +1,22 @@ +/* + * 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_hw_events; + const char **hw_events; + int num_results; + + 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/csrcs/runtimes/host/perf_topdown.h b/src/xtc/csrcs/runtimes/host/perf_topdown.h new file mode 100644 index 00000000..2732c0c7 --- /dev/null +++ b/src/xtc/csrcs/runtimes/host/perf_topdown.h @@ -0,0 +1,12 @@ +#ifndef _PERF_TOPDOWN_H +#define _PERF_TOPDOWN_H + +#include + +extern void perf_topdown_init(void); +extern void perf_topdown_fini(void); +extern void perf_topdown_start(void); +extern void perf_topdown_stop(void); +extern void perf_topdown_dump(FILE *file); + +#endif diff --git a/src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c b/src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c new file mode 100644 index 00000000..c87d2e6f --- /dev/null +++ b/src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c @@ -0,0 +1,231 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "perf_topdown.h" + +/* Ref to: https://github.com/torvalds/linux/blob/master/tools/perf/Documentation/topdown.txt */ +#define RDPMC_FIXED (1 << 30) /* return fixed counters */ +#define RDPMC_METRIC (1 << 29) /* return metric counters */ +#define FIXED_COUNTER_SLOTS 3 +#define METRIC_COUNTER_TOPDOWN_L1_L2 0 +#define SLOTS_PER_CYCLES 4 // todo : arch dependent ? + +#define GET_METRIC(m, i) (((m) >> (i*8)) & 0xff) + +/* L1 Topdown metric events */ +#define TOPDOWN_RETIRING(val) ((double)GET_METRIC(val, 0) / 0xff) +#define TOPDOWN_BAD_SPEC(val) ((double)GET_METRIC(val, 1) / 0xff) +#define TOPDOWN_FE_BOUND(val) ((double)GET_METRIC(val, 2) / 0xff) +#define TOPDOWN_BE_BOUND(val) ((double)GET_METRIC(val, 3) / 0xff) + +/* + * L2 Topdown metric events. + * Available on Sapphire Rapids and later platforms. + */ +#define TOPDOWN_HEAVY_OPS(val) ((double)GET_METRIC(val, 4) / 0xff) +#define TOPDOWN_BR_MISPREDICT(val) ((double)GET_METRIC(val, 5) / 0xff) +#define TOPDOWN_FETCH_LAT(val) ((double)GET_METRIC(val, 6) / 0xff) +#define TOPDOWN_MEM_BOUND(val) ((double)GET_METRIC(val, 7) / 0xff) + +/* 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); */ +/* } */ + +static int sys_perf_event_open(struct perf_event_attr *hw_event, + pid_t pid, int cpu, int group_fd, + unsigned long flags) +{ + long fd; + fd = syscall(SYS_perf_event_open, hw_event, pid, cpu, + group_fd, flags); + if (fd < 0) { + perror("sys_perf_event_open failure"); + exit(EXIT_FAILURE); + } + return (int)fd; +} + +/* static void *map_perf_fd(int perf_fd) */ +/* { */ +/* void *ptr; */ +/* ptr = mmap(0, getpagesize(), PROT_READ, MAP_SHARED, perf_fd, 0); */ +/* if (ptr == NULL) { */ +/* perror("map_perf_fd failure"); */ +/* exit(EXIT_FAILURE); */ +/* } */ +/* return ptr; */ +/* } */ + +/* static void unmap_perf_fd(void *ptr) */ +/* { */ +/* munmap(ptr, getpagesize()); */ +/* } */ + +struct perf_topdown_handle_s { + uint64_t slots_start; + uint64_t slots_stop; + //uint64_t metrics_start; + //uint64_t metrics_stop; + uint64_t retiring_start; + uint64_t retiring_stop; + uint64_t fe_bound_start; + uint64_t fe_bound_stop; + uint64_t issued_start; + uint64_t issued_stop; + uint64_t bad_spec_start; + uint64_t bad_spec_stop; + int slots_fd; + int retiring_fd; + int fe_bound_fd; + int issued_fd; + int bad_spec_fd; + //int metrics_fd; + //void *slots_p; + //void *metrics_p; +}; +typedef struct perf_topdown_handle_s perf_topdown_handle_t; + +static perf_topdown_handle_t global_handle; + +static inline uint64_t read_perf_fd(int fd) +{ + uint64_t value; + ssize_t n; + n = read(fd, &value, sizeof(value)); + assert(n == sizeof(value)); + return value; +} + +static inline void read_perf_group_fd(int fd, uint64_t *counters, int num) +{ + uint64_t fields[1 + num]; + ssize_t n; + n = read(fd, &fields, sizeof(fields)); + assert(n == sizeof(fields)); + assert(fields[0] == num); + for (int c = 0; c < num; c++) { + counters[c] = fields[1+c]; + } +} + +void perf_topdown_init(void) +{ + perf_topdown_handle_t *perf = &global_handle; + struct perf_event_attr slots_attrs = { + .type = PERF_TYPE_RAW, + .size = sizeof(struct perf_event_attr), + .config = 0x003c, //0x400, topdown-total-slots X86_CONFIG(.event=0x3c) + .exclude_kernel = 1, + .read_format = PERF_FORMAT_GROUP, + }; + /* struct perf_event_attr metrics_attrs = { */ + /* .type = PERF_TYPE_RAW, */ + /* .size = sizeof(struct perf_event_attr), */ + /* .config = 0x02c2, //0x8000, // retiring */ + /* .exclude_kernel = 1, */ + /* }; */ + struct perf_event_attr retiring_attrs = { + .type = PERF_TYPE_RAW, + .size = sizeof(struct perf_event_attr), + .config = 0x02c2, // topdown-slots-retired X86_CONFIG(.umask=2, .event=0xc2) + .exclude_kernel = 1, + .read_format = PERF_FORMAT_GROUP, + }; + struct perf_event_attr issued_attrs = { + .type = PERF_TYPE_RAW, + .size = sizeof(struct perf_event_attr), + .config = 0x010e, // topdown-slots-issued X86_CONFIG(.umask=1, .event=0x0e) + .exclude_kernel = 1, + .read_format = PERF_FORMAT_GROUP, + }; + struct perf_event_attr fe_bound_attrs = { + .type = PERF_TYPE_RAW, + .size = sizeof(struct perf_event_attr), + .config = 0x019c, // topdown-fetch-bubbles X86_CONFIG(.umask=1, .event=0x9c) + .exclude_kernel = 1, + .read_format = PERF_FORMAT_GROUP, + }; + struct perf_event_attr bad_spec_attrs = { + .type = PERF_TYPE_RAW, + .size = sizeof(struct perf_event_attr), + .config = 0x0100019d, // topdown-recovery-bubbles X86_CONFIG(.cmask=1, .umask=1, .event=0x9d) + .exclude_kernel = 1, + .read_format = PERF_FORMAT_GROUP, + }; + memset(perf, 0, sizeof(*perf)); + perf->slots_fd = sys_perf_event_open(&slots_attrs, 0, -1, -1, 0); + perf->retiring_fd = sys_perf_event_open(&retiring_attrs, 0, -1, perf->slots_fd, 0); + perf->fe_bound_fd = sys_perf_event_open(&fe_bound_attrs, 0, -1, perf->slots_fd, 0); + perf->issued_fd = sys_perf_event_open(&issued_attrs, 0, -1, perf->slots_fd, 0); + perf->bad_spec_fd = sys_perf_event_open(&bad_spec_attrs, 0, -1, perf->slots_fd, 0); +} + +void perf_topdown_fini(void) +{ + perf_topdown_handle_t *perf = &global_handle; + close(perf->bad_spec_fd); + close(perf->issued_fd); + close(perf->fe_bound_fd); + close(perf->retiring_fd); + close(perf->slots_fd); +} + +void perf_topdown_start(void) +{ + perf_topdown_handle_t *perf = &global_handle; + uint64_t counters[5]; + read_perf_group_fd(perf->slots_fd, counters, sizeof(counters)/sizeof(*counters)); + perf->slots_start = counters[0]; + perf->retiring_start = counters[1]; + perf->fe_bound_start = counters[2]; + perf->issued_start = counters[3]; + perf->bad_spec_start = counters[4]; +} + +void perf_topdown_stop(void) +{ + perf_topdown_handle_t *perf = &global_handle; + uint64_t counters[5]; + read_perf_group_fd(perf->slots_fd, counters, sizeof(counters)/sizeof(*counters)); + perf->slots_stop = counters[0]; + perf->retiring_stop = counters[1]; + perf->fe_bound_stop = counters[2]; + perf->issued_stop = counters[3]; + perf->bad_spec_stop = counters[4]; +} + +void perf_topdown_dump(FILE *file) +{ + perf_topdown_handle_t *perf = &global_handle; + uint64_t cycles = perf->slots_stop - perf->slots_start; + uint64_t slots = cycles * SLOTS_PER_CYCLES; // assume Skylake, Broadwell, Cascade Lake arch + uint64_t retiring = perf->retiring_stop - perf->retiring_start; + uint64_t bad_spec = perf->bad_spec_stop - perf->bad_spec_start; + uint64_t fe_bound = perf->fe_bound_stop - perf->fe_bound_start; + uint64_t issued = perf->issued_stop - perf->issued_start; + uint64_t be_bound = slots - retiring - fe_bound - bad_spec; + double r_retiring = (double)retiring / slots; + double r_bad_spec = (double)bad_spec / slots; + double r_fe_bound = (double)fe_bound / slots; + double r_issued = (double)issued / slots; + double r_be_bound = (double)be_bound / slots; + fprintf(file, "Cycles: %lu\n", cycles); + fprintf(file, "Slots: %lu (%.2f%%)\n", slots, 100.0); + fprintf(file, "Issued: %lu (%.2f%%)\n", issued, r_issued*100); + fprintf(file, "Retiring: %lu (%.2f%%)\n", retiring, r_retiring*100); + fprintf(file, "FE_bound: %lu (%.2f%%)\n", fe_bound, r_fe_bound*100); + fprintf(file, "Bad_spec: %lu (%.2f%%)\n", bad_spec, r_bad_spec*100); + fprintf(file, "BE_bound: %lu (%.2f%%)\n", be_bound, r_be_bound*100); +} diff --git a/src/xtc/itf/runtime/common.py b/src/xtc/itf/runtime/common.py index f42b4c76..d56596c2 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 2fc3a4e1..1ca468c9 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/host/HostRuntime.py b/src/xtc/runtimes/host/HostRuntime.py index 36ff5430..d13bb98d 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 4f10bf02..26ccc814 100644 --- a/src/xtc/runtimes/host/runtime.py +++ b/src/xtc/runtimes/host/runtime.py @@ -156,10 +156,12 @@ 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", "evaluate_flops.c", + "perf_topdown_x86_64.c", "perf_event_darwin.c" if platform == "darwin" else "perf_event_linux.c", ] top_dir = Path(__file__).parents[2] @@ -175,7 +177,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 -O0 -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/utils/evaluation.py b/src/xtc/utils/evaluation.py index d0fceb6f..66c7446c 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -144,6 +144,9 @@ def validate_outputs( return ([], 1, "Error in validation: outputs differ") return ([], 0, "") +DERIVED_METRICS_SIZES = { + "TopdownL1": 4 +} def evaluate_performance( func: Callable[[Any], Any], @@ -157,10 +160,15 @@ 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 + values_num = 0 + for counter in pmu_counters: + values_num += DERIVED_METRICS_SIZES.get(counter, 1) + # FIXME check if the PMU 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))() if cfunc.is_packed: args_array_packed = (CArgValue * len(args_tuples))( @@ -180,12 +188,12 @@ 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( + runtime.evaluate_perf( + results_array, pmu_counters, repeat, number, @@ -194,6 +202,8 @@ def evaluate_performance( args_array, len(args_array), ) + print(f"results: {[round(x, 2) for x in results_array]}") + eval_results = [float(x) for x in results_array] return (eval_results, 0, "") 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 00000000..28a79e2f --- /dev/null +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -0,0 +1,58 @@ +# 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 sys import platform + +#I, J, K, dtype = 4, 32, 512, "float32" +I, J, K, dtype = 1024, 2048, 4096, "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") + +graph = gb.graph + +impl = Backend(graph) + +sch = impl.get_scheduler() +sch.tile("i", {"i1": 2}) +sch.tile("j", {"j1": 16}) +sch.interchange(["k", "i", "j", "i1", "j1"]) +sch.vectorize(["j1"]) +sch.unroll({"i1": 2}) +sched = sch.schedule() + +comp = impl.get_compiler( + shared_lib=True, + dump_file="matmul_mlir", +) +module = comp.compile(sched) + +tma_counters = [] + +# Linux Perf counters +if platform == "linux": + tma_counters += [ + "TopdownL1", + ] +elif platform == "darwin": + # On MacOS, requires sudo to get counters + # TODO: should be tested ideally + tma_counters = [] + + +evaluator = module.get_evaluator( + validate=True, + pmu_counters=tma_counters, +) +results, code, error = evaluator.evaluate() +print(f"CODE: {code}") +print(f"counters: {tma_counters}") +print(f"results: {[int(x) for x in results]}") +# CHECK: CODE: 0 +# CHECK-NEXT: counters: +# CHECK-NEXT: results: From 55fd193c84da7e3276cab11fedf0d6b1cb46b81c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 21 May 2026 12:11:11 +0200 Subject: [PATCH 03/92] [TMA]tma TopdownL1 support for zen4 arch (untested) --- src/xtc/csrcs/runtimes/host/perf_event.h | 2 +- .../csrcs/runtimes/host/perf_event_linux.c | 62 ++++++++++++++----- src/xtc/csrcs/runtimes/host/perf_metrics.c | 47 +++++++++++++- 3 files changed, 92 insertions(+), 19 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event.h b/src/xtc/csrcs/runtimes/host/perf_event.h index 0b4157e9..aaa66742 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 f5b79003..549f964b 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -212,7 +212,50 @@ void stop_perf_events(int n_events, const int *fds, uint64_t *results) { #endif /* HAS_GPU */ } +static int inline set_config_by_arch(const char *name, perf_event_args_t *event){ + if (strncmp(name, "@skl_", 5) == 0) { + event->mode = PERF_ARG_GENERIC; + event->args.config_pair.type = PERF_TYPE_RAW; + + if (strcmp(name, "@skl_slots") == 0) + event->args.config_pair.event = 0x003c; + else if (strcmp(name, "@skl_fe_bound") == 0) + event->args.config_pair.event = 0x019c; + else if (strcmp(name, "@skl_issued") == 0) + event->args.config_pair.event = 0x010e; + else if (strcmp(name, "@skl_retiring") == 0) + event->args.config_pair.event = 0x02c2; + else if (strcmp(name, "@skl_recovery") == 0) + event->args.config_pair.event = 0x0100019d; + else + return 1; + + return 0; + } + + else if (strncmp(name, "@zen4_", 6) == 0) { + event->mode = PERF_ARG_GENERIC; + event->args.config_pair.type = PERF_TYPE_RAW; + + if (strcmp(name, "@zen4_cyc") == 0) + event->args.config_pair.event = 0x0076; + else if (strcmp(name, "@zen4_fe") == 0) + event->args.config_pair.event = 0x1000001A0ULL; + else if (strcmp(name, "@zen4_disp") == 0) + event->args.config_pair.event = 0x07AA; + else if (strcmp(name, "@zen4_ret") == 0) + event->args.config_pair.event = 0x00C1; + else + return 1; // unknow event + + return 0; + } + + return -1; +} + int get_perf_event_config(const char *name, perf_event_args_t *event) { + printf("[DEBUG] full name : %s\n",name); for (int e = 0; e < sizeof(perf_events_decl) / sizeof(*perf_events_decl); e++) { if (strcmp(name, perf_events_decl[e].name) == 0) { @@ -223,25 +266,14 @@ int get_perf_event_config(const char *name, perf_event_args_t *event) { } } - if (strncmp(name, "@skl_", 5) == 0) { - event->mode = PERF_ARG_GENERIC; - event->args.config_pair.type = PERF_TYPE_RAW; - - if (strcmp(name, "@skl_slots") == 0) event->args.config_pair.event = 0x003c; - else if (strcmp(name, "@skl_fe_bound") == 0) event->args.config_pair.event = 0x019c; - else if (strcmp(name, "@skl_issued") == 0) event->args.config_pair.event = 0x010e; - else if (strcmp(name, "@skl_retiring") == 0) event->args.config_pair.event = 0x02c2; - else if (strcmp(name, "@skl_recovery") == 0) event->args.config_pair.event = 0x0100019d; - else return 1; - - return 0; -} + int arch_specific_config = set_config_by_arch(name,event); + if(arch_specific_config != -1) return arch_specific_config; - #if HAS_GPU +#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)); diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 1da67e3c..c8abeb85 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -219,8 +219,9 @@ static void compute_skl_tma_l1(const double *raw_values, double *final_results) 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]; // recovery + double bad_spec_raw = raw_values[4]; + // Pipeline width = 4 on Intel Skylake double slots = 4.0 * cycles; if (slots > 0) { @@ -238,6 +239,42 @@ static void compute_skl_tma_l1(const double *raw_values, double *final_results) } } +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 void compute_amd_zen4_tma_l1(const double *raw_values, double *final_results) { + double cycles = raw_values[0]; + double fe_raw = raw_values[1]; + double disp_raw = raw_values[2]; + double ret_raw = raw_values[3]; + + // Pipeline width = 6 on AMD Zen 4 + double slots = 6.0 * 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); + //if (r_be_bound < 0.0) r_be_bound = 0.0; + assert(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; + } +} + + int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { //compute_test_tma_guil(); if (strcmp(metric_name, "TopdownL1") == 0) { @@ -255,8 +292,12 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } else if (is_amd) { - // todo - return 0; + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 4; + out_resolver->hw_events = amd_zen4_tma_l1_events; + out_resolver->num_results = 4; + out_resolver->compute_formula = compute_amd_zen4_tma_l1; + return 1; } else if (is_arm) { // todo From b9b8ee1218ddead99260a2eb7ab00f8089e247cb Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 26 May 2026 10:49:15 +0200 Subject: [PATCH 04/92] [TMA]microarch detection for tma --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 185 +++++++++++++++++---- 1 file changed, 152 insertions(+), 33 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index c8abeb85..34f150ec 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -21,6 +21,100 @@ #define ARCH_IS_ARM 0 #endif +#if ARCH_IS_X86 +#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 { + INTEL_UNKNOWN = 0, + INTEL_SKYLAKE_CASCADE, + INTEL_ICELAKE_SAPPHIRE +} intel_arch_t; + +intel_arch_t detect_intel_microarchitecture(void) { +#if ARCH_IS_X86 + int family, model; + get_cpu_family_model(&family, &model); + + if (family == 6) { + switch (model) { + // Arch without TMA counter + case 0x4E: case 0x5E: case 0x55: // Skylake-X / Cascade Lake-X / Cooper Lake + case 0x8E: case 0x9E: // Kaby Lake / Coffee Lake + case 0x3D: case 0x47: case 0x4F: // Broadwell + case 0x56: // Broadwell-DE + return INTEL_SKYLAKE_CASCADE; + + // Arch with TMA counter + case 0x7E: case 0x7D: case 0x9D: // Ice Lake + case 0x6A: case 0x6C: case 0x8C: // Ice Lake SP/D + case 0x8F: // Sapphire Rapids + case 0x97: case 0x9A: // Alder Lake + case 0xB7: case 0xBA: case 0xBF: // Raptor Lake + case 0xAA: case 0xAC: // Meteor Lake + return INTEL_ICELAKE_SAPPHIRE; + } + } +#endif + return INTEL_UNKNOWN; +} + +typedef enum { + AMD_UNKNOWN = 0, + AMD_ZEN_1_2, + AMD_ZEN_3, + AMD_ZEN_4 +} amd_arch_t; + +amd_arch_t detect_amd_microarchitecture(void) { +#if ARCH_IS_X86 + int family, model; + get_cpu_family_model(&family, &model); + + if (family == 0x17) { + // Zen 1 : Naples, Summit Ridge + // Zen+ : Pinnacle Ridge + // Zen 2 : Rome, Matisse, Naples + return AMD_ZEN_1_2; + } + + if (family == 0x19) { + if (model >= 0x10 && model <= 0x1F) return AMD_ZEN_4; // Genoa + if (model >= 0x60 && model <= 0x7F) return AMD_ZEN_4; // Phoenix/Dragon Range + if (model >= 0xA0 && model <= 0xAF) return AMD_ZEN_4; // Bergamo + + // Todo : double check if missing models + return AMD_ZEN_3; // (Milan, Vermeer...) + } + // todo family 0x1A for Zen 5 (Turin, Granite Ridge) +#endif + return AMD_UNKNOWN; +} #define GET_METRIC(m, i) (((m) >> (i*8)) & 0xff) @@ -246,24 +340,25 @@ static const char *amd_zen4_tma_l1_events[] = { "@zen4_ret" // 0x00C1 (Ops retired) }; -static void compute_amd_zen4_tma_l1(const double *raw_values, double *final_results) { +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]; - // Pipeline width = 6 on AMD Zen 4 - double slots = 6.0 * cycles; + 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; + //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); - //if (r_be_bound < 0.0) r_be_bound = 0.0; - assert(r_be_bound >= 0.0); + //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; @@ -274,43 +369,67 @@ static void compute_amd_zen4_tma_l1(const double *raw_values, double *final_resu } } +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 +} + int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { - //compute_test_tma_guil(); if (strcmp(metric_name, "TopdownL1") == 0) { - int is_intel = detect_if_intel(); - int is_amd = detect_if_amd(); - int is_arm = detect_if_arm(); - assert(is_intel + is_amd + is_arm == 1); - - if (is_intel) { - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 5; - out_resolver->hw_events = skl_tma_l1_events; - out_resolver->num_results = 4; - out_resolver->compute_formula = compute_skl_tma_l1; - return 1; + + if (detect_if_intel()) { + intel_arch_t uarch = detect_intel_microarchitecture(); + + if (uarch == INTEL_SKYLAKE_CASCADE) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 5; + out_resolver->hw_events = skl_tma_l1_events; + out_resolver->num_results = 4; + out_resolver->compute_formula = compute_skl_tma_l1; + return 1; + } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { + // todo : use native intel tma perf counter + return 0; + } } - else if (is_amd) { - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 4; - out_resolver->hw_events = amd_zen4_tma_l1_events; - out_resolver->num_results = 4; - out_resolver->compute_formula = compute_amd_zen4_tma_l1; - return 1; + else if (detect_if_amd()) { + amd_arch_t uarch = detect_amd_microarchitecture(); + + if (uarch == AMD_ZEN_4) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 4; + out_resolver->hw_events = amd_zen4_tma_l1_events; + out_resolver->num_results = 4; + out_resolver->compute_formula = compute_amd_zen34_tma_l1; + return 1; + } + else if (uarch == AMD_ZEN_1_2) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 4; + out_resolver->hw_events = amd_zen1_tma_l1_events; // Événements Zen 1 à définir + out_resolver->num_results = 4; + out_resolver->compute_formula = compute_amd_zen1_tma_l1; // Largeur 4 + return 1; + } + // else if (uarch == AMD_ZEN_2) ... } - else if (is_arm) { + else if (detect_if_arm()) { // todo + // fopen /sys/devices/system/cpu/cpu0/regs/identification/midr_el1 + // 0x410fd0c0 == Cortex-X1 + // 0x610f2200 == Apple M1 return 0; } - - // Fallback : call perf stat in subprocess ? - return 0; // Unsuported hardware } - - return 0; // Unknow metric + fprintf(stderr,"[DEBUG] Unsuported hardware\n"); + return 0; // Unsuported hardware } + int get_perf_metric_results_count(const char *metric_name) { metric_resolver_t resolver; if (resolve_metric(metric_name, &resolver) && resolver.is_supported) { From c526d5065c596ce58e63b490f5cd6dd2b8733625 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 26 May 2026 11:10:28 +0200 Subject: [PATCH 05/92] [TMA]script to activate HW counters --- activate_hw_counters.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 activate_hw_counters.sh diff --git a/activate_hw_counters.sh b/activate_hw_counters.sh new file mode 100755 index 00000000..b4517a7b --- /dev/null +++ b/activate_hw_counters.sh @@ -0,0 +1,2 @@ +sudo sysctl kernel.perf_event_paranoid=-1 +echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog From 1db64dd4c08d4685096495ad0f01ac40c442b345 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 26 May 2026 11:26:05 +0200 Subject: [PATCH 06/92] [TMA]Hadware Events for modern Intel --- .../csrcs/runtimes/host/perf_event_linux.c | 17 +++++++ src/xtc/csrcs/runtimes/host/perf_metrics.c | 45 ++++++++++++++++--- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 549f964b..d92df76e 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -213,6 +213,8 @@ void stop_perf_events(int n_events, const int *fds, uint64_t *results) { } static int inline set_config_by_arch(const char *name, perf_event_args_t *event){ + + // Old Intel if (strncmp(name, "@skl_", 5) == 0) { event->mode = PERF_ARG_GENERIC; event->args.config_pair.type = PERF_TYPE_RAW; @@ -251,6 +253,21 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) return 0; } + // Modern Intel + else if (strncmp(name, "@icl_", 5) == 0) { + event->mode = PERF_ARG_GENERIC; + event->args.config_pair.type = PERF_TYPE_RAW; + + if (strcmp(name, "@icl_slots") == 0) event->args.config_pair.event = 0x0400; + else if (strcmp(name, "@icl_retiring") == 0) event->args.config_pair.event = 0x8000; + else if (strcmp(name, "@icl_bad_spec") == 0) event->args.config_pair.event = 0x8100; + else if (strcmp(name, "@icl_fe_bound") == 0) event->args.config_pair.event = 0x8200; + else if (strcmp(name, "@icl_be_bound") == 0) event->args.config_pair.event = 0x8300; + else return 1; // Inconnu + + return 0; + } + return -1; } diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 34f150ec..8d94ec54 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -65,7 +65,7 @@ intel_arch_t detect_intel_microarchitecture(void) { switch (model) { // Arch without TMA counter case 0x4E: case 0x5E: case 0x55: // Skylake-X / Cascade Lake-X / Cooper Lake - case 0x8E: case 0x9E: // Kaby Lake / Coffee Lake + case 0x8E: case 0x9E: // Whiskey Lake / Kaby Lake / Coffee Lake case 0x3D: case 0x47: case 0x4F: // Broadwell case 0x56: // Broadwell-DE return INTEL_SKYLAKE_CASCADE; @@ -333,6 +333,31 @@ static void compute_skl_tma_l1(const double *raw_values, double *final_results) } } +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 *amd_zen4_tma_l1_events[] = { "@zen4_cyc", // 0x0076 (Cycles) "@zen4_fe", // 0x1000001A0 (Dispatch slots empty because frontend is stalled) @@ -382,9 +407,11 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { if (strcmp(metric_name, "TopdownL1") == 0) { if (detect_if_intel()) { + fprintf(stderr,"[DEBUG] INTEL detected\n"); intel_arch_t uarch = detect_intel_microarchitecture(); if (uarch == INTEL_SKYLAKE_CASCADE) { + fprintf(stderr,"[DEBUG] Old Intel detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 5; out_resolver->hw_events = skl_tma_l1_events; @@ -392,14 +419,20 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->compute_formula = compute_skl_tma_l1; return 1; } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - // todo : use native intel tma perf counter - return 0; + fprintf(stderr,"[DEBUG] Modern Intel detected\n"); + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 5; + out_resolver->hw_events = intel_modern_tma_l1_events; + out_resolver->num_results = 4; + out_resolver->compute_formula = compute_intel_modern_tma_l1; + return 1; } } else if (detect_if_amd()) { amd_arch_t uarch = detect_amd_microarchitecture(); if (uarch == AMD_ZEN_4) { + fprintf(stderr,"[DEBUG] AMD_ZEN_4 detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 4; out_resolver->hw_events = amd_zen4_tma_l1_events; @@ -408,16 +441,18 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } else if (uarch == AMD_ZEN_1_2) { + fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 4; - out_resolver->hw_events = amd_zen1_tma_l1_events; // Événements Zen 1 à définir + out_resolver->hw_events = amd_zen4_tma_l1_events; out_resolver->num_results = 4; - out_resolver->compute_formula = compute_amd_zen1_tma_l1; // Largeur 4 + out_resolver->compute_formula = compute_amd_zen34_tma_l1; //compute_amd_zen1_tma_l1; return 1; } // else if (uarch == AMD_ZEN_2) ... } else if (detect_if_arm()) { + fprintf(stderr,"[DEBUG] ARM detected\n"); // todo // fopen /sys/devices/system/cpu/cpu0/regs/identification/midr_el1 // 0x410fd0c0 == Cortex-X1 From 73b8cbd585e9d57d48b67bc116318482ed2247f1 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 26 May 2026 14:54:18 +0200 Subject: [PATCH 07/92] [TMA]TopDownL2 support for modern Intel --- .../csrcs/runtimes/host/perf_event_linux.c | 8 +- src/xtc/csrcs/runtimes/host/perf_metrics.c | 198 ++++++++---------- src/xtc/utils/evaluation.py | 3 +- .../evaluation/test_matmul_tma_counters.py | 28 ++- 4 files changed, 123 insertions(+), 114 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index d92df76e..463edc2e 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -259,11 +259,17 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.type = PERF_TYPE_RAW; if (strcmp(name, "@icl_slots") == 0) event->args.config_pair.event = 0x0400; + // TopDownL1 else if (strcmp(name, "@icl_retiring") == 0) event->args.config_pair.event = 0x8000; else if (strcmp(name, "@icl_bad_spec") == 0) event->args.config_pair.event = 0x8100; else if (strcmp(name, "@icl_fe_bound") == 0) event->args.config_pair.event = 0x8200; else if (strcmp(name, "@icl_be_bound") == 0) event->args.config_pair.event = 0x8300; - else return 1; // Inconnu + // TopDownL2 + else if (strcmp(name, "@icl_heavy_ops") == 0) event->args.config_pair.event = 0x8400; + else if (strcmp(name, "@icl_br_mispredict") == 0) event->args.config_pair.event = 0x8500; + else if (strcmp(name, "@icl_fetch_lat") == 0) event->args.config_pair.event = 0x8600; + else if (strcmp(name, "@icl_mem_bound") == 0) event->args.config_pair.event = 0x8700; + else return 1; return 0; } diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 8d94ec54..49b93d60 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -191,115 +191,6 @@ int detect_if_arm(void) { return ARCH_IS_ARM; } -#include -#include -static void compute_test_tma_doc(){ - uint64_t slots_a = read_slots(); - uint64_t metric_a = read_metrics(); - - { - srand(time(NULL)); - for(int i = 0; i<100000;i++){ - asm("":::"memory"); - volatile int a = rand(); - asm("":::"memory"); - - } - } - - uint64_t slots_b = read_slots(); - uint64_t metric_b = read_metrics(); - - // compute scaled metrics for measurement a - uint64_t retiring_slots_a = GET_METRIC(metric_a, 0) * slots_a; - uint64_t bad_spec_slots_a = GET_METRIC(metric_a, 1) * slots_a; - uint64_t fe_bound_slots_a = GET_METRIC(metric_a, 2) * slots_a; - uint64_t be_bound_slots_a = GET_METRIC(metric_a, 3) * slots_a; - - // compute delta scaled metrics between b and a - uint64_t retiring_slots = GET_METRIC(metric_b, 0) * slots_b - retiring_slots_a; - uint64_t bad_spec_slots = GET_METRIC(metric_b, 1) * slots_b - bad_spec_slots_a; - uint64_t fe_bound_slots = GET_METRIC(metric_b, 2) * slots_b - fe_bound_slots_a; - uint64_t be_bound_slots = GET_METRIC(metric_b, 3) * slots_b - be_bound_slots_a; - - uint64_t slots_delta = slots_b - slots_a; - float retiring_ratio = (float)retiring_slots / slots_delta; - float bad_spec_ratio = (float)bad_spec_slots / slots_delta; - float fe_bound_ratio = (float)fe_bound_slots / slots_delta; - float be_bound_ratio = (float)be_bound_slots / slots_delta; - - printf("Retiring %.2f%% Bad Speculation %.2f%% FE Bound %.2f%% BE Bound %.2f%%\n", - retiring_ratio * 100., - bad_spec_ratio * 100., - fe_bound_ratio * 100., - be_bound_ratio * 100.); - - /* - The individual ratios of L2 metric events for the measurement period can be - recreated from L1 and L2 metric counters. (Available on Sapphire Rapids and - later platforms) - */ - - // compute scaled metrics for measurement a - uint64_t heavy_ops_slots_a = GET_METRIC(metric_a, 4) * slots_a; - uint64_t br_mispredict_slots_a = GET_METRIC(metric_a, 5) * slots_a; - uint64_t fetch_lat_slots_a = GET_METRIC(metric_a, 6) * slots_a; - uint64_t mem_bound_slots_a = GET_METRIC(metric_a, 7) * slots_a; - - // compute delta scaled metrics between b and a - uint64_t heavy_ops_slots = GET_METRIC(metric_b, 4) * slots_b - heavy_ops_slots_a; - uint64_t br_mispredict_slots = GET_METRIC(metric_b, 5) * slots_b - br_mispredict_slots_a; - uint64_t fetch_lat_slots = GET_METRIC(metric_b, 6) * slots_b - fetch_lat_slots_a; - uint64_t mem_bound_slots = GET_METRIC(metric_b, 7) * slots_b - mem_bound_slots_a; - - slots_delta = slots_b - slots_a; - float heavy_ops_ratio = (float)heavy_ops_slots / slots_delta; - float light_ops_ratio = retiring_ratio - heavy_ops_ratio; - - float br_mispredict_ratio = (float)br_mispredict_slots / slots_delta; - float machine_clears_ratio = bad_spec_ratio - br_mispredict_ratio; - - float fetch_lat_ratio = (float)fetch_lat_slots / slots_delta; - float fetch_bw_ratio = fe_bound_ratio - fetch_lat_ratio; - - float mem_bound_ratio = (float)mem_bound_slots / slots_delta; - float core_bound_ratio = be_bound_ratio - mem_bound_ratio; - - printf("Heavy Operations %.2f%% Light Operations %.2f%% " - "Branch Mispredict %.2f%% Machine Clears %.2f%% " - "Fetch Latency %.2f%% Fetch Bandwidth %.2f%% " - "Mem Bound %.2f%% Core Bound %.2f%%\n", - heavy_ops_ratio * 100., - light_ops_ratio * 100., - br_mispredict_ratio * 100., - machine_clears_ratio * 100., - fetch_lat_ratio * 100., - fetch_bw_ratio * 100., - mem_bound_ratio * 100., - core_bound_ratio * 100.); - -} - -#include "perf_topdown.h" -static void compute_test_tma_guil(){ - perf_topdown_init(); - perf_topdown_start(); - - { - srand(time(NULL)); - for(int i = 0; i<100000;i++){ - asm("":::"memory"); - volatile int a = rand(); - asm("":::"memory"); - - } - } - - perf_topdown_stop(); - perf_topdown_dump(fdopen(1, "w")); - perf_topdown_fini(); -} - static const char *skl_tma_l1_events[] = { "@skl_slots", // 0x003c (Cycles) "@skl_fe_bound", // 0x019c @@ -358,6 +249,56 @@ static void compute_intel_modern_tma_l1(const double *raw_values, double *final_ } } +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 char *amd_zen4_tma_l1_events[] = { "@zen4_cyc", // 0x0076 (Cycles) "@zen4_fe", // 0x1000001A0 (Dispatch slots empty because frontend is stalled) @@ -402,7 +343,21 @@ 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 } - +/* + * Tested TopdownL1 : + * - INTEL_SKYLAKE_CASCADE (skylake) + * - INTEL_ICELAKE_SAPPHIRE (raptor lake) + * + * Untested : + * - AMD_ZEN_1_2 + * - AMD_ZEN_4 + * + * Not implemented : + * - Aarch64 + * - Zen 3 + * + * + */ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { if (strcmp(metric_name, "TopdownL1") == 0) { @@ -449,7 +404,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->compute_formula = compute_amd_zen34_tma_l1; //compute_amd_zen1_tma_l1; return 1; } - // else if (uarch == AMD_ZEN_2) ... + // else if (uarch == AMD_ZEN_3) ... } else if (detect_if_arm()) { fprintf(stderr,"[DEBUG] ARM detected\n"); @@ -460,6 +415,27 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 0; } } + else if (strcmp(metric_name, "TopdownL2") == 0) { + if (detect_if_intel()) { + intel_arch_t uarch = detect_intel_microarchitecture(); + + if (uarch == INTEL_SKYLAKE_CASCADE) { + fprintf(stderr,"[DEBUG] Unsuported INTEL_SKYLAKE_CASCADE for L2\n"); + // todo : need multiplexing + return 0; + } + else if (uarch == INTEL_ICELAKE_SAPPHIRE) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 9; + out_resolver->hw_events = intel_modern_tma_l2_events; + out_resolver->num_results = 8; + out_resolver->compute_formula = compute_intel_modern_tma_l2; + return 1; + } + } + return 0; + } + fprintf(stderr,"[DEBUG] Unsuported hardware\n"); return 0; // Unsuported hardware } diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index 66c7446c..a1acd5c1 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -145,7 +145,8 @@ def validate_outputs( return ([], 0, "") DERIVED_METRICS_SIZES = { - "TopdownL1": 4 + "TopdownL1": 4, + "TopdownL2": 8 } def evaluate_performance( diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index 28a79e2f..c54e6f99 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -52,7 +52,33 @@ results, code, error = evaluator.evaluate() print(f"CODE: {code}") print(f"counters: {tma_counters}") -print(f"results: {[int(x) for x in results]}") +print(f"results TopDownL1: {[int(x) for x in results]}") + +print("=============\n") + +tma_counters = [] + +# Linux Perf counters +if platform == "linux": + tma_counters += [ + "TopdownL2", + ] +elif platform == "darwin": + # On MacOS, requires sudo to get counters + # TODO: should be tested ideally + tma_counters = [] + + +evaluator = module.get_evaluator( + validate=True, + pmu_counters=tma_counters, +) +results, code, error = evaluator.evaluate() +print(f"CODE: {code}") +print(f"counters: {tma_counters}") +print(f"results TopDownL2: {[int(x) for x in results]}") + + # CHECK: CODE: 0 # CHECK-NEXT: counters: # CHECK-NEXT: results: From 1d588a5c9e788e95fe03122cb3a9d4f581bf7fc1 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 27 May 2026 09:11:00 +0200 Subject: [PATCH 08/92] [TMA]Documentation for tma counters --- src/xtc/csrcs/runtimes/host/perf_event_linux.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 463edc2e..a56721bd 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -212,6 +212,15 @@ void stop_perf_events(int n_events, const int *fds, uint64_t *results) { #endif /* HAS_GPU */ } +/* + * Source + * Intel : https://github.com/torvalds/linux/blob/master/arch/x86/events/intel/core.c + * AMD : https://github.com/torvalds/linux/blob/master/arch/x86/events/amd/core.c + * + * 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 int inline set_config_by_arch(const char *name, perf_event_args_t *event){ // Old Intel From 3d72855642a56f1bbdcf01de4c3e4cef370ec3e8 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 27 May 2026 09:13:14 +0200 Subject: [PATCH 09/92] [TMA]HW counters fallback if API unavailable, now call perf stat [TMA]tma group mapping for topdownL[1-6] --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 1 + src/xtc/csrcs/runtimes/host/perf_topdown.h | 12 - .../csrcs/runtimes/host/perf_topdown_x86_64.c | 231 ------------------ src/xtc/runtimes/host/runtime.py | 1 - src/xtc/utils/evaluation.py | 68 +++++- .../evaluation/test_matmul_tma_counters.py | 27 +- 6 files changed, 92 insertions(+), 248 deletions(-) delete mode 100644 src/xtc/csrcs/runtimes/host/perf_topdown.h delete mode 100644 src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 49b93d60..41e86b26 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "perf_metrics.h" diff --git a/src/xtc/csrcs/runtimes/host/perf_topdown.h b/src/xtc/csrcs/runtimes/host/perf_topdown.h deleted file mode 100644 index 2732c0c7..00000000 --- a/src/xtc/csrcs/runtimes/host/perf_topdown.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef _PERF_TOPDOWN_H -#define _PERF_TOPDOWN_H - -#include - -extern void perf_topdown_init(void); -extern void perf_topdown_fini(void); -extern void perf_topdown_start(void); -extern void perf_topdown_stop(void); -extern void perf_topdown_dump(FILE *file); - -#endif diff --git a/src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c b/src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c deleted file mode 100644 index c87d2e6f..00000000 --- a/src/xtc/csrcs/runtimes/host/perf_topdown_x86_64.c +++ /dev/null @@ -1,231 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "perf_topdown.h" - -/* Ref to: https://github.com/torvalds/linux/blob/master/tools/perf/Documentation/topdown.txt */ -#define RDPMC_FIXED (1 << 30) /* return fixed counters */ -#define RDPMC_METRIC (1 << 29) /* return metric counters */ -#define FIXED_COUNTER_SLOTS 3 -#define METRIC_COUNTER_TOPDOWN_L1_L2 0 -#define SLOTS_PER_CYCLES 4 // todo : arch dependent ? - -#define GET_METRIC(m, i) (((m) >> (i*8)) & 0xff) - -/* L1 Topdown metric events */ -#define TOPDOWN_RETIRING(val) ((double)GET_METRIC(val, 0) / 0xff) -#define TOPDOWN_BAD_SPEC(val) ((double)GET_METRIC(val, 1) / 0xff) -#define TOPDOWN_FE_BOUND(val) ((double)GET_METRIC(val, 2) / 0xff) -#define TOPDOWN_BE_BOUND(val) ((double)GET_METRIC(val, 3) / 0xff) - -/* - * L2 Topdown metric events. - * Available on Sapphire Rapids and later platforms. - */ -#define TOPDOWN_HEAVY_OPS(val) ((double)GET_METRIC(val, 4) / 0xff) -#define TOPDOWN_BR_MISPREDICT(val) ((double)GET_METRIC(val, 5) / 0xff) -#define TOPDOWN_FETCH_LAT(val) ((double)GET_METRIC(val, 6) / 0xff) -#define TOPDOWN_MEM_BOUND(val) ((double)GET_METRIC(val, 7) / 0xff) - -/* 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); */ -/* } */ - -static int sys_perf_event_open(struct perf_event_attr *hw_event, - pid_t pid, int cpu, int group_fd, - unsigned long flags) -{ - long fd; - fd = syscall(SYS_perf_event_open, hw_event, pid, cpu, - group_fd, flags); - if (fd < 0) { - perror("sys_perf_event_open failure"); - exit(EXIT_FAILURE); - } - return (int)fd; -} - -/* static void *map_perf_fd(int perf_fd) */ -/* { */ -/* void *ptr; */ -/* ptr = mmap(0, getpagesize(), PROT_READ, MAP_SHARED, perf_fd, 0); */ -/* if (ptr == NULL) { */ -/* perror("map_perf_fd failure"); */ -/* exit(EXIT_FAILURE); */ -/* } */ -/* return ptr; */ -/* } */ - -/* static void unmap_perf_fd(void *ptr) */ -/* { */ -/* munmap(ptr, getpagesize()); */ -/* } */ - -struct perf_topdown_handle_s { - uint64_t slots_start; - uint64_t slots_stop; - //uint64_t metrics_start; - //uint64_t metrics_stop; - uint64_t retiring_start; - uint64_t retiring_stop; - uint64_t fe_bound_start; - uint64_t fe_bound_stop; - uint64_t issued_start; - uint64_t issued_stop; - uint64_t bad_spec_start; - uint64_t bad_spec_stop; - int slots_fd; - int retiring_fd; - int fe_bound_fd; - int issued_fd; - int bad_spec_fd; - //int metrics_fd; - //void *slots_p; - //void *metrics_p; -}; -typedef struct perf_topdown_handle_s perf_topdown_handle_t; - -static perf_topdown_handle_t global_handle; - -static inline uint64_t read_perf_fd(int fd) -{ - uint64_t value; - ssize_t n; - n = read(fd, &value, sizeof(value)); - assert(n == sizeof(value)); - return value; -} - -static inline void read_perf_group_fd(int fd, uint64_t *counters, int num) -{ - uint64_t fields[1 + num]; - ssize_t n; - n = read(fd, &fields, sizeof(fields)); - assert(n == sizeof(fields)); - assert(fields[0] == num); - for (int c = 0; c < num; c++) { - counters[c] = fields[1+c]; - } -} - -void perf_topdown_init(void) -{ - perf_topdown_handle_t *perf = &global_handle; - struct perf_event_attr slots_attrs = { - .type = PERF_TYPE_RAW, - .size = sizeof(struct perf_event_attr), - .config = 0x003c, //0x400, topdown-total-slots X86_CONFIG(.event=0x3c) - .exclude_kernel = 1, - .read_format = PERF_FORMAT_GROUP, - }; - /* struct perf_event_attr metrics_attrs = { */ - /* .type = PERF_TYPE_RAW, */ - /* .size = sizeof(struct perf_event_attr), */ - /* .config = 0x02c2, //0x8000, // retiring */ - /* .exclude_kernel = 1, */ - /* }; */ - struct perf_event_attr retiring_attrs = { - .type = PERF_TYPE_RAW, - .size = sizeof(struct perf_event_attr), - .config = 0x02c2, // topdown-slots-retired X86_CONFIG(.umask=2, .event=0xc2) - .exclude_kernel = 1, - .read_format = PERF_FORMAT_GROUP, - }; - struct perf_event_attr issued_attrs = { - .type = PERF_TYPE_RAW, - .size = sizeof(struct perf_event_attr), - .config = 0x010e, // topdown-slots-issued X86_CONFIG(.umask=1, .event=0x0e) - .exclude_kernel = 1, - .read_format = PERF_FORMAT_GROUP, - }; - struct perf_event_attr fe_bound_attrs = { - .type = PERF_TYPE_RAW, - .size = sizeof(struct perf_event_attr), - .config = 0x019c, // topdown-fetch-bubbles X86_CONFIG(.umask=1, .event=0x9c) - .exclude_kernel = 1, - .read_format = PERF_FORMAT_GROUP, - }; - struct perf_event_attr bad_spec_attrs = { - .type = PERF_TYPE_RAW, - .size = sizeof(struct perf_event_attr), - .config = 0x0100019d, // topdown-recovery-bubbles X86_CONFIG(.cmask=1, .umask=1, .event=0x9d) - .exclude_kernel = 1, - .read_format = PERF_FORMAT_GROUP, - }; - memset(perf, 0, sizeof(*perf)); - perf->slots_fd = sys_perf_event_open(&slots_attrs, 0, -1, -1, 0); - perf->retiring_fd = sys_perf_event_open(&retiring_attrs, 0, -1, perf->slots_fd, 0); - perf->fe_bound_fd = sys_perf_event_open(&fe_bound_attrs, 0, -1, perf->slots_fd, 0); - perf->issued_fd = sys_perf_event_open(&issued_attrs, 0, -1, perf->slots_fd, 0); - perf->bad_spec_fd = sys_perf_event_open(&bad_spec_attrs, 0, -1, perf->slots_fd, 0); -} - -void perf_topdown_fini(void) -{ - perf_topdown_handle_t *perf = &global_handle; - close(perf->bad_spec_fd); - close(perf->issued_fd); - close(perf->fe_bound_fd); - close(perf->retiring_fd); - close(perf->slots_fd); -} - -void perf_topdown_start(void) -{ - perf_topdown_handle_t *perf = &global_handle; - uint64_t counters[5]; - read_perf_group_fd(perf->slots_fd, counters, sizeof(counters)/sizeof(*counters)); - perf->slots_start = counters[0]; - perf->retiring_start = counters[1]; - perf->fe_bound_start = counters[2]; - perf->issued_start = counters[3]; - perf->bad_spec_start = counters[4]; -} - -void perf_topdown_stop(void) -{ - perf_topdown_handle_t *perf = &global_handle; - uint64_t counters[5]; - read_perf_group_fd(perf->slots_fd, counters, sizeof(counters)/sizeof(*counters)); - perf->slots_stop = counters[0]; - perf->retiring_stop = counters[1]; - perf->fe_bound_stop = counters[2]; - perf->issued_stop = counters[3]; - perf->bad_spec_stop = counters[4]; -} - -void perf_topdown_dump(FILE *file) -{ - perf_topdown_handle_t *perf = &global_handle; - uint64_t cycles = perf->slots_stop - perf->slots_start; - uint64_t slots = cycles * SLOTS_PER_CYCLES; // assume Skylake, Broadwell, Cascade Lake arch - uint64_t retiring = perf->retiring_stop - perf->retiring_start; - uint64_t bad_spec = perf->bad_spec_stop - perf->bad_spec_start; - uint64_t fe_bound = perf->fe_bound_stop - perf->fe_bound_start; - uint64_t issued = perf->issued_stop - perf->issued_start; - uint64_t be_bound = slots - retiring - fe_bound - bad_spec; - double r_retiring = (double)retiring / slots; - double r_bad_spec = (double)bad_spec / slots; - double r_fe_bound = (double)fe_bound / slots; - double r_issued = (double)issued / slots; - double r_be_bound = (double)be_bound / slots; - fprintf(file, "Cycles: %lu\n", cycles); - fprintf(file, "Slots: %lu (%.2f%%)\n", slots, 100.0); - fprintf(file, "Issued: %lu (%.2f%%)\n", issued, r_issued*100); - fprintf(file, "Retiring: %lu (%.2f%%)\n", retiring, r_retiring*100); - fprintf(file, "FE_bound: %lu (%.2f%%)\n", fe_bound, r_fe_bound*100); - fprintf(file, "Bad_spec: %lu (%.2f%%)\n", bad_spec, r_bad_spec*100); - fprintf(file, "BE_bound: %lu (%.2f%%)\n", be_bound, r_be_bound*100); -} diff --git a/src/xtc/runtimes/host/runtime.py b/src/xtc/runtimes/host/runtime.py index 26ccc814..9b684e07 100644 --- a/src/xtc/runtimes/host/runtime.py +++ b/src/xtc/runtimes/host/runtime.py @@ -161,7 +161,6 @@ def _compile_runtime(out_dll: str, tdir: str, runtime_type: RuntimeType): "alloc.c", "fclock.c", "evaluate_flops.c", - "perf_topdown_x86_64.c", "perf_event_darwin.c" if platform == "darwin" else "perf_event_linux.c", ] top_dir = Path(__file__).parents[2] diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index a1acd5c1..16c06434 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -6,6 +6,12 @@ from xtc.itf.graph import Graph import ctypes import numpy as np +import subprocess +import shutil +import os +import signal +import time +from typing import Callable, Any from xtc.utils.numpy import np_init from xtc.runtimes.types.ndarray import NDArray from xtc.graphs.xtc.graph import XTCGraph @@ -144,9 +150,15 @@ def validate_outputs( return ([], 1, "Error in validation: outputs differ") return ([], 0, "") + +# perf list --no-desc | grep -i -E -A 59 tma_L[1-4]_group: DERIVED_METRICS_SIZES = { - "TopdownL1": 4, - "TopdownL2": 8 + "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 + "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 } def evaluate_performance( @@ -205,9 +217,61 @@ def evaluate_performance( ) print(f"results: {[round(x, 2) for x in results_array]}") eval_results = [float(x) for x in results_array] + + needs_fallback = False + if len(pmu_counters) > 0: + if eval_results[0] == -1.0 or all(x == 0.0 for x in eval_results): + needs_fallback = True + + # Fallback call perf in subprocess + if needs_fallback: + print(f"[WARNING] Hardware not supported by internal resolver. Fallback to 'perf stat' for {pmu_counters}...") + + perf_path = shutil.which("perf") + if not perf_path: + return ([], 1, "perf tool not found in PATH") + + metric_str = ",".join(pmu_counters) + my_pid = str(os.getpid()) + + # cmd: perf stat -p -M + cmd = [perf_path, "stat", "-p", my_pid, "-M", metric_str] + + try: + print("[DEBUG] Starting perf...") + perf_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # time to hook to Python + time.sleep(0.5) + + # Rerun evaluation without HW counters + dummy_results = (ctypes.c_double * repeat)() + if cfunc.is_packed: + print("[DEBUG] Rerun packed...") + _ = runtime.evaluate_packed_perf(dummy_results, [], repeat, number, min_repeat_ms, cfunc, args_array_packed, args_codes_packed, len(args_tuples)) + else: + print("[DEBUG] Rerun not packed...") + runtime.evaluate_perf(dummy_results, [], repeat, number, min_repeat_ms, cfunc, args_array, len(args_array)) + + print("[DEBUG] Stoping perf...") + perf_proc.send_signal(signal.SIGINT) + _, stderr_output = perf_proc.communicate(timeout=5.0) + + print("\n====== Fallback 'perf stat' Output ======\n") + print(stderr_output) + print("===================================\n") + + return ([], 0, "Fallback used: printed to stderr") + + except Exception as e: + print(f"[DEBUG] Fallback perf stat failed : {e}") + return ([], 1, f"Fallback perf stat failed: {e}") + + # Return API result return (eval_results, 0, "") + def copy_outputs( parameters: tuple[list[NDArray], list[NDArray]], target_parameters: tuple[list[NDArray], list[NDArray]], diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index c54e6f99..84e77f18 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -37,7 +37,7 @@ # Linux Perf counters if platform == "linux": tma_counters += [ - "TopdownL1", + "TopdownL1" ] elif platform == "darwin": # On MacOS, requires sudo to get counters @@ -61,7 +61,7 @@ # Linux Perf counters if platform == "linux": tma_counters += [ - "TopdownL2", + "TopdownL2" ] elif platform == "darwin": # On MacOS, requires sudo to get counters @@ -78,6 +78,29 @@ print(f"counters: {tma_counters}") print(f"results TopDownL2: {[int(x) for x in results]}") +print("=============\n") + +tma_counters = [] + +# Linux Perf counters +if platform == "linux": + tma_counters += [ + "TopdownL3" + ] +elif platform == "darwin": + # On MacOS, requires sudo to get counters + # TODO: should be tested ideally + tma_counters = [] + + +evaluator = module.get_evaluator( + validate=True, + pmu_counters=tma_counters, +) +results, code, error = evaluator.evaluate() +print(f"CODE: {code}") +print(f"counters: {tma_counters}") +print(f"results TopDownL2: {[int(x) for x in results]}") # CHECK: CODE: 0 # CHECK-NEXT: counters: From 755bbd6851c57b61e3222abc777409f09f90a96c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 28 May 2026 12:05:29 +0200 Subject: [PATCH 10/92] [TMA]test sizes for tma eval --- tests/filecheck/evaluation/test_matmul_tma_counters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index 84e77f18..43cb6fd6 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -5,8 +5,9 @@ from xtc.backends.mlir import Backend from sys import platform -#I, J, K, dtype = 4, 32, 512, "float32" -I, J, K, dtype = 1024, 2048, 4096, "float32" +#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") From 83b7c2a6b4c42cd08d57808d5d3a79911bd073d1 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 28 May 2026 12:06:12 +0200 Subject: [PATCH 11/92] [TMA]Marimo tutorial for hardware counters --- docs/tutorials/hw_counters_introduction.py | 311 +++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 docs/tutorials/hw_counters_introduction.py diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py new file mode 100644 index 00000000..ea06d61d --- /dev/null +++ b/docs/tutorials/hw_counters_introduction.py @@ -0,0 +1,311 @@ +import marimo + +__generated_with = "0.19.6" +app = marimo.App(width="full") + +with app.setup: + import os + import marimo as mo + from sys import platform + + notebook_dir = os.path.dirname(os.path.realpath(__file__)) + project_root = os.path.abspath(os.path.join(notebook_dir, "..", "..")) + os.chdir(project_root) + + +@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. While theoretical complexity (Big O) is useful, modern CPUs are incredibly complex beasts with deep pipelines, multiple cache levels, and speculative execution. + + In this notebook, we will use **XTC** to compile a Matrix Multiplication and evaluate it using: + 1. **Raw PMU Counters** (Performance Monitoring Units) to count exact hardware events. + 2. **Top-down Microarchitecture Analysis (TMA)** to identify actual bottlenecks. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + > **Disclaimer and prerequisites:** + > Results will vary depending on your hardware architecture (MacOS is currently not supported). + > If your microarchitecture is not explicitly mapped by the internal C resolver, the system will gracefully fallback to the `perf stat` command-line tool. + > + > To make hardware counters available to userspace applications (ring 3), run this in your terminal: + > ```bash + > sudo sysctl kernel.perf_event_paranoid=-1 + > ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1. Defining the Workload & Schedule (Interactive) + First, we define a medium-sized matrix multiplication (1024x2048x4096). + + **Play with the sliders below!** Modifying the tiling sizes or the unroll factor will dynamically recompile the MLIR code and update the hardware counters in real-time. You will see the bottlenecks shift! + (default: i=3 j=24 unroll=2) + + """) + return + + +@app.cell +def _(mo): + # Interactive UI elements for the schedule + tile_i_ui = mo.ui.slider(start=1, stop=16, step=1, value=3, label="Tile I (Rows)") + tile_j_ui = mo.ui.slider(start=8, stop=128, step=8, value=24, label="Tile J (Cols)") + unroll_ui = mo.ui.slider(start=1, stop=8, step=1, value=2, label="Unroll factor") + + schedule_ui = mo.hstack([tile_i_ui, tile_j_ui, unroll_ui]) + return schedule_ui, tile_i_ui, tile_j_ui, unroll_ui + + +@app.cell +def _(schedule_ui): + schedule_ui + return + + +@app.cell +def _(tile_i_ui, tile_j_ui, unroll_ui): + import xtc.graphs.xtc.op as O + from xtc.backends.mlir import Backend + + # 1024x2048x4096 is large enough to show memory bottlenecks, + # but small enough to evaluate quickly. + I, J, K, dtype = 1024, 2048, 4096, "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") + + # Scheduling using interactive values + impl = Backend(gb.graph) + sch = impl.get_scheduler() + sch.tile("i", {"i1": tile_i_ui.value}) + sch.tile("j", {"j1": tile_j_ui.value}) + sch.interchange(["k", "i", "j", "i1", "j1"]) + sch.vectorize(["j1"]) + sch.unroll({"i1": unroll_ui.value}) + sched = sch.schedule() + + # Compilation + comp = impl.get_compiler(shared_lib=True, dump_file="matmul_mlir") + module = comp.compile(sched) + return I, J, K, a, b, comp, dtype, gb, impl, module, sch, sched + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 2. Raw Hardware Counters (PMU) + CPUs expose raw counters to track specific events. We can ask the CPU exactly how many cycles were spent or how many L1/L2 cache misses occurred. + + *Note: Raw event names are highly architecture-dependent. `libpfm4` helps translating them.* + """) + return + + +@app.cell +def _(module, mo, platform): + pmu_counters = ["cycles", "instructions"] + + # Adding architecture-specific counters (Linux/x86 usually) + if platform == "linux": + pmu_counters += [ + "mem_load_retired.l1_miss", + "mem_load_retired.l2_miss" + ] + + evaluator_pmu = module.get_evaluator( + validate=True, + pmu_counters=pmu_counters, + ) + + results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() + + # Formatting PMU results + _pmu_data = [{"Counter": c, "Value": int(v)} for c, v in zip(pmu_counters, results_pmu)] + + pmu_ui = mo.vstack([ + mo.md(f"**Execution Code:** `{code_pmu}`"), + mo.ui.table(_pmu_data, label="Raw PMU Results") + ]) + return code_pmu, error_pmu, evaluator_pmu, pmu_counters, pmu_ui, results_pmu + + +@app.cell +def _(pmu_ui): + pmu_ui + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 3. Top-down Microarchitecture Analysis (Level 1) + Raw counters are hard to interpret: *Is 5 million cache misses bad? Does it actually stall the CPU?* + + To solve this, **TMA** (Top-down Analysis) groups all CPU pipeline slots into 4 distinct categories, summing up to 100%: + * 🟢 **Retiring:** Good! The CPU is doing useful work (executing our math). + * 🔴 **Bad Speculation:** The CPU guessed a branch wrong and had to flush its pipeline. + * 🔵 **Frontend Bound:** The CPU is starved; it cannot fetch/decode instructions fast enough. + * 🟣 **Backend Bound:** The CPU is waiting (usually for Memory or Execution Units) to finish the current instructions. + """) + return + + +@app.cell +def _(module, mo, platform): + tma_l1_counters = ["TopdownL1"] if platform == "linux" else [] + + evaluator_l1 = module.get_evaluator( + validate=False, + pmu_counters=tma_l1_counters, + ) + + results_l1, code_l1, error_l1 = evaluator_l1.evaluate() + + _l1_labels = ["Retiring", "Bad Speculation", "Frontend Bound", "Backend Bound"] + + if tma_l1_counters and len(results_l1) >= 4: + _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] + _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") + else: + _l1_ui_table = mo.md("*TMA L1 is not supported or returned empty on this machine.*") + + l1_ui = mo.vstack([ + mo.md(f"**Execution Code:** `{code_l1}`"), + _l1_ui_table, + mo.md("> *For a Matrix Multiplication, you should expect a high **Backend Bound** (waiting for RAM/Caches) and a solid **Retiring** percentage.*") + ]) + return code_l1, error_l1, evaluator_l1, l1_ui, results_l1, tma_l1_counters, tma_l1_counters + + +@app.cell +def _(l1_ui): + l1_ui + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 4. Drilling Down (Topdown Level 2) + If our code is heavily **Backend Bound**, we need to know why! Is it because our math is too complex for the ALU/FPU (`Core Bound`), or are we constantly waiting for the RAM (`Memory Bound`)? + + TMA Level 2 splits the L1 categories further. The Backend Bound is split into: + + * **Core Bound:** The pipeline is likely stalled due to a lack of available execution units (ALU/FPU) or data dependencies between instructions preventing out-of-order execution. + * **Memory Bound:** The pipeline is likely stalled due to demand load/store instructions. This represents the fraction of slots where Execution Units are starved because of non-completed in-flight memory demand loads, or occasionally when store buffers are completely full imposing backpressure. + """) + return + + +@app.cell +def _(module, mo, platform): + tma_l2_counters = ["TopdownL2"] if platform == "linux" else [] + + evaluator_l2 = module.get_evaluator( + validate=False, + pmu_counters=tma_l2_counters, + ) + + results_l2, code_l2, error_l2 = evaluator_l2.evaluate() + + _l2_labels = [ + "🟢 Retiring: Light Ops", + "🟢 Retiring: Heavy Ops", + "🔴 Bad Spec: Machine Clears", + "🔴 Bad Spec: Branch Mispredicts", + "🔵 Frontend: Fetch Bandwidth", + "🔵 Frontend: Fetch Latency", + "🟣 Backend: Core Bound", + "🟣 Backend: Memory Bound" + ] + + _fallback_msg = mo.md("") + + if tma_l2_counters and len(results_l2) >= 8: + # Check if the fallback hit (returns -1 on index 0 when internal resolver fails) + if results_l2[0] < 0: + _l2_ui_table = mo.md("⚠️ *Internal C resolver unsupported for L2. System gracefully fell back to `perf stat` output in your terminal.*") + _fallback_msg = mo.callout( + mo.md("### How to read the `perf stat` fallback terminal output\n" + "Since your CPU requires too many events for a single pass, `perf` multiplexes the hardware. " + "Look at your terminal: `perf` automatically computes the percentages next to raw values. " + "Look for indented metrics like `core_bound` and `memory_bound` underneath `backend_bound`."), + kind="warn" + ) + else: + _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] + _l2_ui_table = mo.ui.table(_l2_data, label="Topdown L2 Results") + else: + _l2_ui_table = mo.md("*TMA L2 is not supported or returned empty on this machine.*") + + l2_ui = mo.vstack([ + mo.md(f"**Execution Code:** `{code_l2}`"), + _l2_ui_table, + _fallback_msg + ]) + return code_l2, error_l2, evaluator_l2, l2_ui, results_l2, tma_l2_counters + + +@app.cell +def _(l2_ui): + l2_ui + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## How to fix Core Bound vs Memory Bound? + + Once you know where your bottleneck is, you can adapt your code or your MLIR schedule: + + ### If you are heavily Core Bound: + Your CPU has all the data it needs in the caches, but it cannot crunch the numbers fast enough. + * **Action 1 (Vectorization):** Ensure your loops are properly vectorized so the CPU processes 8 or 16 floats per instruction (AVX2/AVX-512) instead of 1. + * **Action 2 (Instruction-Level Parallelism):** Increase the `Unroll` factor. This breaks data dependency chains and allows the CPU's Out-Of-Order engine to execute multiple independent additions/multiplications at the exact same time. + + ### If you are heavily Memory Bound: + Your CPU's execution units are starving because they are waiting hundreds of cycles for data to arrive from RAM. + * **Action 1 (Cache Locality):** Modify your `Tile` sizes (Tiling/Blocking). The goal is to make sure a chunk of Matrix A and Matrix B fits perfectly inside the ultra-fast L1 or L2 cache before computing it. + * **Action 2 (Memory Access Pattern):** Ensure contiguous memory accesses. If your code jumps around memory (strided access), the hardware prefetcher cannot predict what to load next. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Going deeper: Topdown L3, L4 and beyond... + + Topdown Level 2 is great, but it sometimes leaves us with more questions. If you are 60% *Memory Bound*, you might wonder: *"Am I bound by the L1 cache, the L3 cache, or the Main Memory (DRAM)?"* + + This is where Topdown Level 3 and Level 4 come in (often requiring specific `perf stat -M` metrics depending on your architecture). + + The **Memory Bound** category further splits into: + 1. **L1 Bound (L3):** Data is not in registers, but found extremely quickly in L1 cache. Often caused by high memory latency per instruction or bank conflicts. + 2. **L2 Bound (L3):** Data missed L1 but was found in L2. + 3. **L3 Bound (L3):** Data missed L1/L2 but was found in L3. + 4. **Ext. Memory Bound (L3):** Data missed ALL caches and had to be fetched from Main RAM. This is catastrophic for performance! + * ➡️ *Splits into L4:* **Mem Bandwidth** (the memory bus is saturated) vs **Mem Latency** (waiting for the RAM chip to respond). + 5. **Store Bound (L3):** The CPU's store buffers are full because it is writing too much data to memory too quickly. + + > *Try increasing the Tile sizes in the first cell to something huge (e.g., Tile J = 128). You will see the **Memory Bound** spike because the data chunk no longer fits in the fast L1 cache!* + """) + return + + +if __name__ == "__main__": + app.run() From ba16582873a365de6643fc360abbee92529817a2 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 28 May 2026 16:46:27 +0200 Subject: [PATCH 12/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 475 +++++++++++++++------ src/xtc/utils/evaluation.py | 2 +- 2 files changed, 342 insertions(+), 135 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index ea06d61d..2756b0ce 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -5,6 +5,7 @@ with app.setup: import os + import sys import marimo as mo from sys import platform @@ -14,7 +15,7 @@ @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" # Hardware Performance Counters & Top-down Analysis @@ -28,7 +29,7 @@ def _(mo): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" > **Disclaimer and prerequisites:** > Results will vary depending on your hardware architecture (MacOS is currently not supported). @@ -37,48 +38,41 @@ def _(mo): > To make hardware counters available to userspace applications (ring 3), run this in your terminal: > ```bash > sudo sysctl kernel.perf_event_paranoid=-1 + > echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog > ``` """) return @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" - ## 1. Defining the Workload & Schedule (Interactive) - First, we define a medium-sized matrix multiplication (1024x2048x4096). - - **Play with the sliders below!** Modifying the tiling sizes or the unroll factor will dynamically recompile the MLIR code and update the hardware counters in real-time. You will see the bottlenecks shift! - (default: i=3 j=24 unroll=2) + ## 1. Defining the Workload & Schedule + We define a medium-sized matrix multiplication (1024x2048x4096). + **Use the code editor below** to modify the scheduling specification using the `descript` notation. Once you modify the code, apply the changes to dynamically recompile the MLIR code and update the hardware counters in real-time. """) return @app.cell -def _(mo): +def _(): # Interactive UI elements for the schedule - tile_i_ui = mo.ui.slider(start=1, stop=16, step=1, value=3, label="Tile I (Rows)") - tile_j_ui = mo.ui.slider(start=8, stop=128, step=8, value=24, label="Tile J (Cols)") + tile_i_ui = mo.ui.slider(start=1, stop=64, step=1, value=3, label="Tile I (Rows)") + tile_j_ui = mo.ui.slider(start=8, stop=512, step=8, value=24, label="Tile J (Cols)") unroll_ui = mo.ui.slider(start=1, stop=8, step=1, value=2, label="Unroll factor") schedule_ui = mo.hstack([tile_i_ui, tile_j_ui, unroll_ui]) - return schedule_ui, tile_i_ui, tile_j_ui, unroll_ui + return tile_i_ui, tile_j_ui, unroll_ui @app.cell -def _(schedule_ui): - schedule_ui - return - - -@app.cell -def _(tile_i_ui, tile_j_ui, unroll_ui): - import xtc.graphs.xtc.op as O +def _(): + _editor_code = '''import xtc.graphs.xtc.op as O from xtc.backends.mlir import Backend + from xtc.schedules.descript import descript_scheduler - # 1024x2048x4096 is large enough to show memory bottlenecks, - # but small enough to evaluate quickly. + # Problem setup I, J, K, dtype = 1024, 2048, 4096, "float32" a = O.tensor((I, K), dtype, name="A") @@ -87,59 +81,203 @@ def _(tile_i_ui, tile_j_ui, unroll_ui): with O.graph(name="matmul") as gb: O.matmul(a, b, name="C") - # Scheduling using interactive values - impl = Backend(gb.graph) - sch = impl.get_scheduler() - sch.tile("i", {"i1": tile_i_ui.value}) - sch.tile("j", {"j1": tile_j_ui.value}) - sch.interchange(["k", "i", "j", "i1", "j1"]) - sch.vectorize(["j1"]) - sch.unroll({"i1": unroll_ui.value}) - sched = sch.schedule() + backend = Backend(gb.graph) + + # Schedule specification + # slider_i, slider_j and slider_unroll are magically injected from the UI sliders! + schedule_spec = { + "i": {}, + "j": {}, + "k": {}, + f"i#{slider_i}": {"unroll": slider_unroll}, + f"j#{slider_j}": {"vectorize": True} + } + + # Compile + scheduler = backend.get_scheduler() + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + spec=schedule_spec + ) + sched = scheduler.schedule() + + compiler = backend.get_compiler(dump_file="matmul_mlir", shared_lib=True) + module = compiler.compile(sched) + ''' + descript_editor = mo.ui.code_editor( + value=_editor_code, + language="python", + label="Schedule & Compilation Sandbox" + ) + return (descript_editor,) + + +@app.cell +def _(descript_editor): + descript_editor + return + + +@app.cell +def _(descript_editor, tile_i_ui, tile_j_ui, unroll_ui): + # Execute the user's code safely to extract the module and parameters + local_vars = {} + + # Inject the slider values dynamically into the execution context + global_vars = globals().copy() + global_vars.update({ + "slider_i": tile_i_ui.value, + "slider_j": tile_j_ui.value, + "slider_unroll": unroll_ui.value, + }) + + exec_error = None + module = None + I_val, J_val, K_val = 1024, 2048, 4096 + + try: + exec(descript_editor.value, global_vars, local_vars) + module = local_vars.get("module") + I_val = local_vars.get("I", 1024) + J_val = local_vars.get("J", 2048) + K_val = local_vars.get("K", 4096) + except Exception as e: + exec_error = str(e) + return I_val, J_val, K_val, exec_error, module + + +@app.cell +def _(I_val, J_val, K_val, tile_i_ui, tile_j_ui, unroll_ui): + # Fetch hardware cache sizes dynamically (Linux sysfs) + caches_kb = {} + if sys.platform == "linux" and os.path.exists("/sys/devices/system/cpu/cpu0/cache"): + try: + for i in range(4): # Usually index0 to index3 (L1d, L1i, L2, L3) + path = f"/sys/devices/system/cpu/cpu0/cache/index{i}" + if os.path.exists(path): + with open(f"{path}/level", "r") as f: + level = int(f.read().strip()) + with open(f"{path}/type", "r") as f: + ctype = f.read().strip() + with open(f"{path}/size", "r") as f: + size_str = f.read().strip() + + s_kb = 0 + if size_str.endswith('K'): s_kb = int(size_str[:-1]) + elif size_str.endswith('M'): s_kb = int(size_str[:-1]) * 1024 + else: s_kb = int(size_str) // 1024 + + if level == 1 and ctype == "Data": caches_kb["L1d"] = s_kb + elif level == 2: caches_kb["L2"] = s_kb + elif level == 3: caches_kb["L3"] = s_kb + except Exception: + pass + + # Fallbacks in case reading failed + if "L1d" not in caches_kb: caches_kb["L1d"] = 1 + if "L2" not in caches_kb: caches_kb["L2"] = 1 + if len(caches_kb) <= 2 and "L3" not in caches_kb: caches_kb["L3"] = 1 + + b_size = 4 # float32 (4 bytes) + + def fmt(b): + if b >= 1024**2: return f"{b / 1024**2:.1f} MiB" + if b >= 1024: return f"{b / 1024:.1f} KiB" + return f"{b} B" + + geo_md = f""" + | Tensor | Total Size | 1 Row | 1 Col | + |---|---|---|---| + | **A** ({I_val}×{K_val}) | {fmt(I_val*K_val*b_size)} | {fmt(K_val*b_size)} | {fmt(I_val*b_size)} | + | **B** ({K_val}×{J_val}) | {fmt(K_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(K_val*b_size)} | + | **C** ({I_val}×{J_val}) | {fmt(I_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(I_val*b_size)} | + """ + + # Extra Metrics + tile_c_bytes = tile_i_ui.value * tile_j_ui.value * b_size + flops = 2 * I_val * J_val * K_val + gflops = flops / 1e9 + + def format_cache(name, kb): + mb = kb / 1024 + if kb >= 1024: return f"**{name}** : {mb:.1f} MiB" + return f"**{name}** : {kb:.0f} KiB" + + cache_lines = [ + format_cache("L1 Data", caches_kb.get("L1d", 32)), + format_cache("L2 Cache", caches_kb.get("L2", 1024)) + ] + if "L3" in caches_kb: + cache_lines.append(format_cache("L3 Cache", caches_kb["L3"])) + + cache_md = "
".join([f" {l}" for l in cache_lines]) + + sidebar = mo.sidebar( + mo.vstack([ + mo.md("### Schedule Information"), + mo.md("Modify parameters to update TMA metrics in real-time."), + tile_i_ui, + tile_j_ui, + unroll_ui, + mo.md("---"), + mo.md("### Problem Geometry"), + mo.md(geo_md), + mo.md(f"**Total Math:** `{gflops:.1f} GFLOPs`"), + mo.md(f"**Current C-Tile (i×j):** `{fmt(tile_c_bytes)}`"), + mo.md("---"), + mo.md("### Your CPU Caches"), + mo.md(cache_md), + mo.md("
*Compare row/col sizes to your caches to predict bottlenecks!*"), + mo.md("
*If cache size is 1, parsing of `/sys/devices/system/cpu/cpu0/cache/` failed.*") + ]) + ) + return (sidebar,) - # Compilation - comp = impl.get_compiler(shared_lib=True, dump_file="matmul_mlir") - module = comp.compile(sched) - return I, J, K, a, b, comp, dtype, gb, impl, module, sch, sched + +@app.cell +def _(sidebar): + sidebar + return @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## 2. Raw Hardware Counters (PMU) CPUs expose raw counters to track specific events. We can ask the CPU exactly how many cycles were spent or how many L1/L2 cache misses occurred. - *Note: Raw event names are highly architecture-dependent. `libpfm4` helps translating them.* + *Note: Raw event names are highly architecture-dependent. `libpfm4` helps translating them. The `perf list` command can show you available ones.* """) return @app.cell -def _(module, mo, platform): - pmu_counters = ["cycles", "instructions"] - - # Adding architecture-specific counters (Linux/x86 usually) - if platform == "linux": - pmu_counters += [ - "mem_load_retired.l1_miss", - "mem_load_retired.l2_miss" - ] - - evaluator_pmu = module.get_evaluator( - validate=True, - pmu_counters=pmu_counters, - ) +def _(exec_error, module): + if module is None: + pmu_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") + else: + pmu_counters = ["cycles", "instructions"] - results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() + if platform == "linux": + pmu_counters += [ + "mem_load_retired.l1_miss", + "mem_load_retired.l2_miss" + ] - # Formatting PMU results - _pmu_data = [{"Counter": c, "Value": int(v)} for c, v in zip(pmu_counters, results_pmu)] + evaluator_pmu = module.get_evaluator( + validate=True, + pmu_counters=pmu_counters, + ) + results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() - pmu_ui = mo.vstack([ - mo.md(f"**Execution Code:** `{code_pmu}`"), - mo.ui.table(_pmu_data, label="Raw PMU Results") - ]) - return code_pmu, error_pmu, evaluator_pmu, pmu_counters, pmu_ui, results_pmu + _pmu_data = [{"Counter": c, "Value": int(v)} for c, v in zip(pmu_counters, results_pmu)] + pmu_ui = mo.vstack([ + mo.md(f"**Execution Code:** `{code_pmu}`"), + mo.ui.table(_pmu_data, label="Raw PMU Results") + ]) + return (pmu_ui,) @app.cell @@ -149,7 +287,7 @@ def _(pmu_ui): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## 3. Top-down Microarchitecture Analysis (Level 1) Raw counters are hard to interpret: *Is 5 million cache misses bad? Does it actually stall the CPU?* @@ -164,30 +302,32 @@ def _(mo): @app.cell -def _(module, mo, platform): - tma_l1_counters = ["TopdownL1"] if platform == "linux" else [] - - evaluator_l1 = module.get_evaluator( - validate=False, - pmu_counters=tma_l1_counters, - ) - - results_l1, code_l1, error_l1 = evaluator_l1.evaluate() - - _l1_labels = ["Retiring", "Bad Speculation", "Frontend Bound", "Backend Bound"] - - if tma_l1_counters and len(results_l1) >= 4: - _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] - _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") +def _(exec_error, module): + if module is None: + l1_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: - _l1_ui_table = mo.md("*TMA L1 is not supported or returned empty on this machine.*") + tma_l1_counters = ["TopdownL1"] if platform == "linux" else [] + evaluator_l1 = module.get_evaluator(validate=False, pmu_counters=tma_l1_counters) + results_l1, code_l1, error_l1 = evaluator_l1.evaluate() + + _l1_labels = ["Retiring", "Bad Speculation", "Frontend Bound", "Backend Bound"] + + if tma_l1_counters and len(results_l1) > 0: + if results_l1[0] < 0: + _l1_ui_table = mo.accordion({ + "Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```") + }) + else: + _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] + _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") + else: + _l1_ui_table = mo.md("*TMA L1 is not supported on this machine.*") - l1_ui = mo.vstack([ - mo.md(f"**Execution Code:** `{code_l1}`"), - _l1_ui_table, - mo.md("> *For a Matrix Multiplication, you should expect a high **Backend Bound** (waiting for RAM/Caches) and a solid **Retiring** percentage.*") - ]) - return code_l1, error_l1, evaluator_l1, l1_ui, results_l1, tma_l1_counters, tma_l1_counters + l1_ui = mo.vstack([ + mo.md(f"**Execution Code:** `{code_l1}`"), + _l1_ui_table + ]) + return (l1_ui,) @app.cell @@ -197,7 +337,7 @@ def _(l1_ui): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## 4. Drilling Down (Topdown Level 2) If our code is heavily **Backend Bound**, we need to know why! Is it because our math is too complex for the ALU/FPU (`Core Bound`), or are we constantly waiting for the RAM (`Memory Bound`)? @@ -211,52 +351,42 @@ def _(mo): @app.cell -def _(module, mo, platform): - tma_l2_counters = ["TopdownL2"] if platform == "linux" else [] - - evaluator_l2 = module.get_evaluator( - validate=False, - pmu_counters=tma_l2_counters, - ) - - results_l2, code_l2, error_l2 = evaluator_l2.evaluate() - - _l2_labels = [ - "🟢 Retiring: Light Ops", - "🟢 Retiring: Heavy Ops", - "🔴 Bad Spec: Machine Clears", - "🔴 Bad Spec: Branch Mispredicts", - "🔵 Frontend: Fetch Bandwidth", - "🔵 Frontend: Fetch Latency", - "🟣 Backend: Core Bound", - "🟣 Backend: Memory Bound" - ] +def _(exec_error, module): + if module is None: + l2_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") + else: + tma_l2_counters = ["TopdownL2"] if platform == "linux" else [] + evaluator_l2 = module.get_evaluator(validate=False, pmu_counters=tma_l2_counters) + results_l2, code_l2, error_l2 = evaluator_l2.evaluate() + + _l2_labels = [ + "🟢 Retiring: Light Ops", + "🟢 Retiring: Heavy Ops", + "🔴 Bad Spec: Machine Clears", + "🔴 Bad Spec: Branch Mispredicts", + "🔵 Frontend: Fetch Bandwidth", + "🔵 Frontend: Fetch Latency", + "🟣 Backend: Core Bound", + "🟣 Backend: Memory Bound" + ] - _fallback_msg = mo.md("") - - if tma_l2_counters and len(results_l2) >= 8: - # Check if the fallback hit (returns -1 on index 0 when internal resolver fails) - if results_l2[0] < 0: - _l2_ui_table = mo.md("⚠️ *Internal C resolver unsupported for L2. System gracefully fell back to `perf stat` output in your terminal.*") - _fallback_msg = mo.callout( - mo.md("### How to read the `perf stat` fallback terminal output\n" - "Since your CPU requires too many events for a single pass, `perf` multiplexes the hardware. " - "Look at your terminal: `perf` automatically computes the percentages next to raw values. " - "Look for indented metrics like `core_bound` and `memory_bound` underneath `backend_bound`."), - kind="warn" - ) + if tma_l2_counters and len(results_l2) > 0: + if results_l2[0] < 0: + _l2_ui_table = mo.vstack([ + mo.callout(mo.md("⚠️ *Internal C resolver unsupported for L2. Showing `perf stat` output below.*"), kind="warn"), + mo.accordion({"Terminal Output from `perf`": mo.md(f"```text\n{error_l2}\n```")}) + ]) + else: + _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] + _l2_ui_table = mo.ui.table(_l2_data, label="Topdown L2 Results") else: - _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] - _l2_ui_table = mo.ui.table(_l2_data, label="Topdown L2 Results") - else: - _l2_ui_table = mo.md("*TMA L2 is not supported or returned empty on this machine.*") + _l2_ui_table = mo.md("*TMA L2 is not supported on this machine.*") - l2_ui = mo.vstack([ - mo.md(f"**Execution Code:** `{code_l2}`"), - _l2_ui_table, - _fallback_msg - ]) - return code_l2, error_l2, evaluator_l2, l2_ui, results_l2, tma_l2_counters + l2_ui = mo.vstack([ + mo.md(f"**Execution Code:** `{code_l2}`"), + _l2_ui_table + ]) + return (l2_ui,) @app.cell @@ -266,7 +396,7 @@ def _(l2_ui): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## How to fix Core Bound vs Memory Bound? @@ -274,38 +404,115 @@ def _(mo): ### If you are heavily Core Bound: Your CPU has all the data it needs in the caches, but it cannot crunch the numbers fast enough. - * **Action 1 (Vectorization):** Ensure your loops are properly vectorized so the CPU processes 8 or 16 floats per instruction (AVX2/AVX-512) instead of 1. - * **Action 2 (Instruction-Level Parallelism):** Increase the `Unroll` factor. This breaks data dependency chains and allows the CPU's Out-Of-Order engine to execute multiple independent additions/multiplications at the exact same time. + * **Vectorization:** Ensure your loops are properly vectorized so the CPU processes 8 or 16 floats per instruction (AVX2/AVX-512) instead of 1. + * **Instruction-Level Parallelism:** Increase the `unroll` factor. This breaks data dependency chains and allows the CPU's Out-Of-Order engine to execute multiple independent additions/multiplications at the exact same time. ### If you are heavily Memory Bound: Your CPU's execution units are starving because they are waiting hundreds of cycles for data to arrive from RAM. - * **Action 1 (Cache Locality):** Modify your `Tile` sizes (Tiling/Blocking). The goal is to make sure a chunk of Matrix A and Matrix B fits perfectly inside the ultra-fast L1 or L2 cache before computing it. - * **Action 2 (Memory Access Pattern):** Ensure contiguous memory accesses. If your code jumps around memory (strided access), the hardware prefetcher cannot predict what to load next. + * **Cache Locality:** Modify your `tile` sizes (Tiling/Blocking). The goal is to make sure a chunk of Matrix A and Matrix B fits perfectly inside the ultra-fast L1 or L2 cache before computing it. + * **Memory Access Pattern:** Ensure contiguous memory accesses. If your code jumps around memory (strided access), the hardware prefetcher cannot predict what to load next. """) return @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## Going deeper: Topdown L3, L4 and beyond... Topdown Level 2 is great, but it sometimes leaves us with more questions. If you are 60% *Memory Bound*, you might wonder: *"Am I bound by the L1 cache, the L3 cache, or the Main Memory (DRAM)?"* This is where Topdown Level 3 and Level 4 come in (often requiring specific `perf stat -M` metrics depending on your architecture). + For the moment depending of your microarchitecture XTC only support up to L2 topdown, the linux perf tool will be needed to go futher. The **Memory Bound** category further splits into: 1. **L1 Bound (L3):** Data is not in registers, but found extremely quickly in L1 cache. Often caused by high memory latency per instruction or bank conflicts. 2. **L2 Bound (L3):** Data missed L1 but was found in L2. 3. **L3 Bound (L3):** Data missed L1/L2 but was found in L3. 4. **Ext. Memory Bound (L3):** Data missed ALL caches and had to be fetched from Main RAM. This is catastrophic for performance! - * ➡️ *Splits into L4:* **Mem Bandwidth** (the memory bus is saturated) vs **Mem Latency** (waiting for the RAM chip to respond). + * *Splits into L4:* **Mem Bandwidth** (the memory bus is saturated) vs **Mem Latency** (waiting for the RAM chip to respond). 5. **Store Bound (L3):** The CPU's store buffers are full because it is writing too much data to memory too quickly. - > *Try increasing the Tile sizes in the first cell to something huge (e.g., Tile J = 128). You will see the **Memory Bound** spike because the data chunk no longer fits in the fast L1 cache!* + > *Try increasing the `tile` values in the code sandbox to something huge. You will see the **Memory Bound** spike because the data chunk no longer fits in the fast caches!* """) return +@app.cell +def _(mo): + _sandbox_default = '''[ + "instructions", + "branches", + "branch-misses", + "L1-dcache-loads", + "L1-dcache-load-misses" +]''' + sandbox_editor = mo.ui.code_editor( + value=_sandbox_default, + language="python", + ) + + sandbox_form = sandbox_editor.form(submit_button_label=" Run Custom Metrics") + return sandbox_editor, sandbox_form + + +@app.cell +def _(mo, sandbox_form): + mo.vstack([ + mo.md(r""" + --- + ## Sandbox: Experiment with your own metrics + + Curious about branch misses or raw L1 cache loads? Write your own list of PMU/TMA events below. + + *Temporary TMA metrics must be evaluate one at time without be mix up with PMU* + + *This cell will not run automatically. You must click the button to evaluate the kernel.* + """), + sandbox_form + ]) + return + + +@app.cell +def _(mo, module, sandbox_form): + mo.stop(sandbox_form.value is None, mo.md("*Click 'Run Custom Metrics' to see the results.*")) + + if module is None: + sandbox_output = mo.md("**Module is not compiled.** Please fix the compilation sandbox above first.") + else: + import ast + try: + # Safely parse the user's string into a Python list + custom_counters = ast.literal_eval(sandbox_form.value) + if not isinstance(custom_counters, list): + raise ValueError("Input must be a Python list.") + + evaluator_sb = module.get_evaluator(validate=False, pmu_counters=custom_counters) + results_sb, code_sb, err_sb = evaluator_sb.evaluate() + + output_lines = [f"**Execution Code:** `{code_sb}`"] + + if err_sb: + output_lines.append(f"**Terminal Output / Errors:**\n```text\n{err_sb}\n```") + + output_lines.append("**Raw Results:**\n```text") + for c, v in zip(custom_counters, results_sb): + output_lines.append(f"{c:35} : {v}") + output_lines.append("```") + + sandbox_output = mo.md("\n".join(output_lines)) + + except Exception as e: + sandbox_output = mo.md(f"**Error parsing your list:** {e}") + + return custom_counters, evaluator_sb, output_lines, results_sb, sandbox_output + +@app.cell +def _(sandbox_output): + sandbox_output + return + + if __name__ == "__main__": app.run() diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index 16c06434..af30ddd2 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -261,7 +261,7 @@ def evaluate_performance( print(stderr_output) print("===================================\n") - return ([], 0, "Fallback used: printed to stderr") + return ([-1.0], 0, stderr_output) except Exception as e: print(f"[DEBUG] Fallback perf stat failed : {e}") From f98de411d15755df70edbabaa24d4acf381d7847 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 1 Jun 2026 09:44:17 +0200 Subject: [PATCH 13/92] [TMA]rename 'tma_counters' to 'hw_counters' sinces they can be mixed up now --- .../evaluation/test_matmul_tma_counters.py | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index 43cb6fd6..ad40d207 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -33,75 +33,52 @@ ) module = comp.compile(sched) -tma_counters = [] +hw_counters = [] # Linux Perf counters if platform == "linux": - tma_counters += [ + hw_counters += [ "TopdownL1" ] elif platform == "darwin": # On MacOS, requires sudo to get counters # TODO: should be tested ideally - tma_counters = [] + hw_counters = [] evaluator = module.get_evaluator( validate=True, - pmu_counters=tma_counters, + pmu_counters=hw_counters, ) results, code, error = evaluator.evaluate() print(f"CODE: {code}") -print(f"counters: {tma_counters}") +print(f"counters: {hw_counters}") print(f"results TopDownL1: {[int(x) for x in results]}") print("=============\n") -tma_counters = [] +hw_counters = ["mem_load_retired.l1_miss","mem_load_retired.l2_miss","mem_load_retired.l3_miss"] # Linux Perf counters if platform == "linux": - tma_counters += [ - "TopdownL2" - ] -elif platform == "darwin": - # On MacOS, requires sudo to get counters - # TODO: should be tested ideally - tma_counters = [] - - -evaluator = module.get_evaluator( - validate=True, - pmu_counters=tma_counters, -) -results, code, error = evaluator.evaluate() -print(f"CODE: {code}") -print(f"counters: {tma_counters}") -print(f"results TopDownL2: {[int(x) for x in results]}") - -print("=============\n") - -tma_counters = [] - -# Linux Perf counters -if platform == "linux": - tma_counters += [ + hw_counters += [ + "TopdownL2", "TopdownL3" ] elif platform == "darwin": # On MacOS, requires sudo to get counters # TODO: should be tested ideally - tma_counters = [] + hw_counters = [] evaluator = module.get_evaluator( validate=True, - pmu_counters=tma_counters, + pmu_counters=hw_counters, ) results, code, error = evaluator.evaluate() print(f"CODE: {code}") -print(f"counters: {tma_counters}") -print(f"results TopDownL2: {[int(x) for x in results]}") +print(f"counters: {hw_counters}") +print(f"results : {[int(x) for x in results]}") # CHECK: CODE: 0 # CHECK-NEXT: counters: From 997e2fbb932690754898cc67a9a10d4a7275723b Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 1 Jun 2026 11:24:44 +0200 Subject: [PATCH 14/92] [TMA]Allow to mix up pmu and tma, fallback to perf only on unsupported metrics --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 157 ++++++++++++-------- src/xtc/utils/evaluation.py | 52 ++++--- 2 files changed, 128 insertions(+), 81 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index e2f705f5..68f11939 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -184,6 +184,12 @@ void evaluate6_perf(double *results, int events_num, const char *events_names[], define_evaluateN(func, arg0, arg1, arg2, arg3, arg4, arg5); } +typedef struct { + int is_derived; + metric_resolver_t resolver; + int hw_offset; + int out_offset; +} metric_map_t; void evaluate_perf(double *results, int events_num, const char *events_names[], int repeat, int number, int min_repeat_ms, @@ -191,82 +197,109 @@ void evaluate_perf(double *results, int events_num, const char *events_names[], { metric_resolver_t resolver; - int is_derived_metric = 0; + int total_hw_events = 0; + int total_out_results = 0; + int has_derived = 0; + metric_map_t *map = NULL; int hw_events_num = events_num; const char **hw_events_names = (const char **)events_names; double *hw_results = results; - // Todo handle multiples tma from user - if (events_num == 1 && resolve_metric(events_names[0], &resolver)) { - if (!resolver.is_supported) { - fprintf(stderr, - "Error : The metric '%s' isn ot supported on this CPU.\n", - events_names[0]); - return; + if (events_num > 0) { + 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)) { + if (!map[i].resolver.is_supported) { + map[i].is_derived = 0; + total_hw_events += 1; + total_out_results += 1; + } else { + map[i].is_derived = 1; + has_derived = 1; + total_hw_events += map[i].resolver.num_hw_events; + total_out_results += map[i].resolver.num_results; + } + } else { + map[i].is_derived = 0; + total_hw_events += 1; + total_out_results += 1; + } } + } - is_derived_metric = 1; - hw_events_num = resolver.num_hw_events; - hw_events_names = resolver.hw_events; + if (has_derived) { + hw_events_names = (const char **)malloc(total_hw_events * sizeof(char *)); + int hw_idx = 0; - hw_results = (double *)malloc(repeat * hw_events_num * sizeof(double)); + for (int i = 0; i < events_num; i++) { + if (map[i].is_derived) { + for (int j = 0; j < map[i].resolver.num_hw_events; j++) { + hw_events_names[hw_idx++] = map[i].resolver.hw_events[j]; + } + } else { + hw_events_names[hw_idx++] = events_names[i]; + } + } + hw_results = (double *)malloc(repeat * total_hw_events * sizeof(double)); + } else { + total_hw_events = events_num; } switch (nargs) { - case 0: - evaluate0_perf(hw_results, hw_events_num, hw_events_names, - repeat, number, min_repeat_ms, - (func0_t)func); - break; - case 1: - evaluate1_perf(hw_results, hw_events_num, hw_events_names, - repeat, number, min_repeat_ms, - (func1_t)func, args[0]); - break; - case 2: - evaluate2_perf(hw_results, hw_events_num, hw_events_names, - repeat, number, min_repeat_ms, - (func2_t)func, args[0], args[1]); - break; - case 3: - evaluate3_perf(hw_results, hw_events_num, hw_events_names, - repeat, number, min_repeat_ms, - (func3_t)func, args[0], args[1], args[2]); - break; - case 4: - evaluate4_perf(hw_results, hw_events_num, hw_events_names, - repeat, number, min_repeat_ms, - (func4_t)func, args[0], args[1], args[2], args[3]); - break; - case 5: - evaluate5_perf(hw_results, hw_events_num, hw_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(hw_results, hw_events_num, hw_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; - } - if (is_derived_metric) { - for (int r = 0; r < repeat; r++) { - const double *run_raw_values = &hw_results[r * hw_events_num]; - double *run_final_results = &results[r * resolver.num_results]; + case 0: + evaluate0_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func0_t)func); + break; + case 1: + evaluate1_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); + break; + case 2: + evaluate2_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); + break; + case 3: + evaluate3_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); + break; + case 4: + evaluate4_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); + break; + case 5: + evaluate5_perf(hw_results, total_hw_events, hw_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(hw_results, total_hw_events, hw_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; + } + if (has_derived) { + 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]; - printf("[DEBUG C] Run %d - Raw hardware counters per call:\n", r); - for (int e = 0; e < hw_events_num; e++) { - printf(" -> %-20s : %f\n", hw_events_names[e], run_raw_values[e]); + if (map[i].is_derived) { + // Sécurité : si le noyau Linux a refusé d'ouvrir l'événement matériel (dépassement limite) + if (run_raw[0] == -1.0) { + 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 { + // Copie simple pour les PMU normaux + run_final[0] = run_raw[0]; + } + } } - - resolver.compute_formula(run_raw_values, run_final_results); + free(hw_events_names); + free(hw_results); } - free(hw_results); - } } void evaluate(double *results, diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index af30ddd2..21d1419d 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -218,33 +218,45 @@ def evaluate_performance( print(f"results: {[round(x, 2) for x in results_array]}") eval_results = [float(x) for x in results_array] - needs_fallback = False - if len(pmu_counters) > 0: - if eval_results[0] == -1.0 or all(x == 0.0 for x in eval_results): - needs_fallback = True + failed_counters = [] + current_idx = 0 + + for counter in pmu_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 call perf in subprocess - if needs_fallback: - print(f"[WARNING] Hardware not supported by internal resolver. Fallback to 'perf stat' for {pmu_counters}...") + # Fallback on linux perf tool + if failed_counters: + print(f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'...") perf_path = shutil.which("perf") if not perf_path: - return ([], 1, "perf tool not found in PATH") + return (eval_results, 1, "perf tool not found in PATH") - metric_str = ",".join(pmu_counters) my_pid = str(os.getpid()) - # cmd: perf stat -p -M - cmd = [perf_path, "stat", "-p", my_pid, "-M", metric_str] + 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] + + cmd = [perf_path, "stat", "-p", my_pid] + + if len(perf_events) > 0: + cmd.extend(["-e", ",".join(perf_events)]) + if len(perf_metrics) > 0: + cmd.extend(["-M", ",".join(perf_metrics)]) try: print("[DEBUG] Starting perf...") perf_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - # time to hook to Python - time.sleep(0.5) + time.sleep(1.5) - # Rerun evaluation without HW counters + # Rerun evaluation without HW counters to generate activity for perf dummy_results = (ctypes.c_double * repeat)() if cfunc.is_packed: print("[DEBUG] Rerun packed...") @@ -253,25 +265,27 @@ def evaluate_performance( print("[DEBUG] Rerun not packed...") runtime.evaluate_perf(dummy_results, [], repeat, number, min_repeat_ms, cfunc, args_array, len(args_array)) - print("[DEBUG] Stoping perf...") + print("[DEBUG] Stopping perf...") perf_proc.send_signal(signal.SIGINT) _, stderr_output = perf_proc.communicate(timeout=5.0) - print("\n====== Fallback 'perf stat' Output ======\n") + 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 ([-1.0], 0, stderr_output) + return (eval_results, 0, formatted_fallback_output) except Exception as e: print(f"[DEBUG] Fallback perf stat failed : {e}") - return ([], 1, f"Fallback perf stat failed: {e}") + return (eval_results, 1, f"Fallback perf stat failed: {e}") # Return API result return (eval_results, 0, "") - def copy_outputs( parameters: tuple[list[NDArray], list[NDArray]], target_parameters: tuple[list[NDArray], list[NDArray]], From 85f5b687a114e233f64e74b6d334f548e20b7bd3 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 1 Jun 2026 12:14:24 +0200 Subject: [PATCH 15/92] [TMA]tma marimo use a compile button instead of dynamicly loading all the file --- docs/tutorials/hw_counters_introduction.py | 112 ++++++++++++--------- 1 file changed, 67 insertions(+), 45 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 2756b0ce..f54b2d0b 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -15,7 +15,7 @@ @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" # Hardware Performance Counters & Top-down Analysis @@ -29,7 +29,7 @@ def _(): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" > **Disclaimer and prerequisites:** > Results will vary depending on your hardware architecture (MacOS is currently not supported). @@ -45,32 +45,34 @@ def _(): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## 1. Defining the Workload & Schedule We define a medium-sized matrix multiplication (1024x2048x4096). - **Use the code editor below** to modify the scheduling specification using the `descript` notation. Once you modify the code, apply the changes to dynamically recompile the MLIR code and update the hardware counters in real-time. + **Use the code editor below** to modify the scheduling specification using the `descript` notation. + + **Interactive Workflow:** + 1. Adjust the sliders in the sidebar to see how the Cache Footprint updates instantly. + 2. Click the **"Compile & Evaluate"** button in the sidebar to apply the schedule and update the TMA metrics. """) return @app.cell -def _(): +def _(mo): # Interactive UI elements for the schedule tile_i_ui = mo.ui.slider(start=1, stop=64, step=1, value=3, label="Tile I (Rows)") tile_j_ui = mo.ui.slider(start=8, stop=512, step=8, value=24, label="Tile J (Cols)") unroll_ui = mo.ui.slider(start=1, stop=8, step=1, value=2, label="Unroll factor") - - schedule_ui = mo.hstack([tile_i_ui, tile_j_ui, unroll_ui]) return tile_i_ui, tile_j_ui, unroll_ui @app.cell -def _(): +def _(mo): _editor_code = '''import xtc.graphs.xtc.op as O - from xtc.backends.mlir import Backend - from xtc.schedules.descript import descript_scheduler +from xtc.backends.mlir import Backend +from xtc.schedules.descript import descript_scheduler # Problem setup I, J, K, dtype = 1024, 2048, 4096, "float32" @@ -121,36 +123,54 @@ def _(descript_editor): @app.cell -def _(descript_editor, tile_i_ui, tile_j_ui, unroll_ui): - # Execute the user's code safely to extract the module and parameters +def _(descript_editor, mo, tile_i_ui, tile_j_ui, unroll_ui): + # State management to isolate heavy computation from slider movement + compile_params, set_compile_params = mo.state(None) + + def _trigger_compile(_): + set_compile_params({ + "code": descript_editor.value, + "i": tile_i_ui.value, + "j": tile_j_ui.value, + "unroll": unroll_ui.value + }) + + compile_btn = mo.ui.button( + label="Compile & Evaluate", + kind="success", + on_click=_trigger_compile + ) + return compile_btn, compile_params, set_compile_params + + +@app.cell +def _(compile_params, mo): + mo.stop(compile_params() is None, mo.md("*👈 Click **'Compile & Evaluate'** in the sidebar to start.*")) + + _params = compile_params() local_vars = {} - # Inject the slider values dynamically into the execution context global_vars = globals().copy() global_vars.update({ - "slider_i": tile_i_ui.value, - "slider_j": tile_j_ui.value, - "slider_unroll": unroll_ui.value, + "slider_i": _params["i"], + "slider_j": _params["j"], + "slider_unroll": _params["unroll"], }) exec_error = None module = None - I_val, J_val, K_val = 1024, 2048, 4096 try: - exec(descript_editor.value, global_vars, local_vars) + exec(_params["code"], global_vars, local_vars) module = local_vars.get("module") - I_val = local_vars.get("I", 1024) - J_val = local_vars.get("J", 2048) - K_val = local_vars.get("K", 4096) except Exception as e: exec_error = str(e) - return I_val, J_val, K_val, exec_error, module + + return exec_error, module @app.cell -def _(I_val, J_val, K_val, tile_i_ui, tile_j_ui, unroll_ui): - # Fetch hardware cache sizes dynamically (Linux sysfs) +def _(compile_btn, mo, os, sys, tile_i_ui, tile_j_ui, unroll_ui): caches_kb = {} if sys.platform == "linux" and os.path.exists("/sys/devices/system/cpu/cpu0/cache"): try: @@ -180,7 +200,9 @@ def _(I_val, J_val, K_val, tile_i_ui, tile_j_ui, unroll_ui): if "L2" not in caches_kb: caches_kb["L2"] = 1 if len(caches_kb) <= 2 and "L3" not in caches_kb: caches_kb["L3"] = 1 - b_size = 4 # float32 (4 bytes) + _I, _J, _K = 1024, 2048, 4096 + b_size = 4 # float32 + # todo : check element size dynamicly def fmt(b): if b >= 1024**2: return f"{b / 1024**2:.1f} MiB" @@ -190,14 +212,13 @@ def fmt(b): geo_md = f""" | Tensor | Total Size | 1 Row | 1 Col | |---|---|---|---| - | **A** ({I_val}×{K_val}) | {fmt(I_val*K_val*b_size)} | {fmt(K_val*b_size)} | {fmt(I_val*b_size)} | - | **B** ({K_val}×{J_val}) | {fmt(K_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(K_val*b_size)} | - | **C** ({I_val}×{J_val}) | {fmt(I_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(I_val*b_size)} | + | **A** ({_I}×{_K}) | {fmt(_I*_K*b_size)} | {fmt(_K*b_size)} | {fmt(_I*b_size)} | + | **B** ({_K}×{_J}) | {fmt(_K*_J*b_size)} | {fmt(_J*b_size)} | {fmt(_K*b_size)} | + | **C** ({_I}×{_J}) | {fmt(_I*_J*b_size)} | {fmt(_J*b_size)} | {fmt(_I*b_size)} | """ - # Extra Metrics tile_c_bytes = tile_i_ui.value * tile_j_ui.value * b_size - flops = 2 * I_val * J_val * K_val + flops = 2 * _I * _J * _K gflops = flops / 1e9 def format_cache(name, kb): @@ -216,8 +237,9 @@ def format_cache(name, kb): sidebar = mo.sidebar( mo.vstack([ - mo.md("### Schedule Information"), - mo.md("Modify parameters to update TMA metrics in real-time."), + compile_btn, + mo.md("---"), + mo.md("### Schedule Controls"), tile_i_ui, tile_j_ui, unroll_ui, @@ -229,8 +251,7 @@ def format_cache(name, kb): mo.md("---"), mo.md("### Your CPU Caches"), mo.md(cache_md), - mo.md("
*Compare row/col sizes to your caches to predict bottlenecks!*"), - mo.md("
*If cache size is 1, parsing of `/sys/devices/system/cpu/cpu0/cache/` failed.*") + mo.md("
*Compare row/col sizes to your caches to predict bottlenecks!*") ]) ) return (sidebar,) @@ -243,7 +264,7 @@ def _(sidebar): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## 2. Raw Hardware Counters (PMU) CPUs expose raw counters to track specific events. We can ask the CPU exactly how many cycles were spent or how many L1/L2 cache misses occurred. @@ -254,7 +275,7 @@ def _(): @app.cell -def _(exec_error, module): +def _(exec_error, mo, module, platform): if module is None: pmu_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: @@ -277,7 +298,7 @@ def _(exec_error, module): mo.md(f"**Execution Code:** `{code_pmu}`"), mo.ui.table(_pmu_data, label="Raw PMU Results") ]) - return (pmu_ui,) + return evaluator_pmu, pmu_counters, pmu_ui, results_pmu @app.cell @@ -287,7 +308,7 @@ def _(pmu_ui): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## 3. Top-down Microarchitecture Analysis (Level 1) Raw counters are hard to interpret: *Is 5 million cache misses bad? Does it actually stall the CPU?* @@ -302,7 +323,7 @@ def _(): @app.cell -def _(exec_error, module): +def _(exec_error, mo, module, platform): if module is None: l1_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: @@ -327,7 +348,7 @@ def _(exec_error, module): mo.md(f"**Execution Code:** `{code_l1}`"), _l1_ui_table ]) - return (l1_ui,) + return code_l1, error_l1, evaluator_l1, l1_ui, results_l1, tma_l1_counters @app.cell @@ -337,7 +358,7 @@ def _(l1_ui): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## 4. Drilling Down (Topdown Level 2) If our code is heavily **Backend Bound**, we need to know why! Is it because our math is too complex for the ALU/FPU (`Core Bound`), or are we constantly waiting for the RAM (`Memory Bound`)? @@ -351,7 +372,7 @@ def _(): @app.cell -def _(exec_error, module): +def _(exec_error, mo, module, platform): if module is None: l2_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: @@ -386,7 +407,7 @@ def _(exec_error, module): mo.md(f"**Execution Code:** `{code_l2}`"), _l2_ui_table ]) - return (l2_ui,) + return code_l2, error_l2, evaluator_l2, l2_ui, results_l2, tma_l2_counters @app.cell @@ -396,7 +417,7 @@ def _(l2_ui): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## How to fix Core Bound vs Memory Bound? @@ -416,7 +437,7 @@ def _(): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## Going deeper: Topdown L3, L4 and beyond... @@ -508,6 +529,7 @@ def _(mo, module, sandbox_form): return custom_counters, evaluator_sb, output_lines, results_sb, sandbox_output + @app.cell def _(sandbox_output): sandbox_output From 817e4645ca37072ba0e4b9f07d4e7ed9558e2982 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 1 Jun 2026 14:28:25 +0200 Subject: [PATCH 16/92] [TMA]less effective schedule to see the backend bound --- docs/tutorials/hw_counters_introduction.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index f54b2d0b..c5e06dc3 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -86,22 +86,20 @@ def _(mo): backend = Backend(gb.graph) # Schedule specification - # slider_i, slider_j and slider_unroll are magically injected from the UI sliders! schedule_spec = { - "i": {}, "j": {}, + "i": {}, "k": {}, - f"i#{slider_i}": {"unroll": slider_unroll}, - f"j#{slider_j}": {"vectorize": True} + f"j#{slider_j}": {"unroll": slider_unroll}, + f"i#{slider_i}": {"vectorize": True} } - # Compile scheduler = backend.get_scheduler() descript_scheduler( - scheduler=scheduler, - node_name="C", - abstract_dims=["i", "j", "k"], - spec=schedule_spec + scheduler=scheduler, + node_name="C", + abstract_dims=["j", "i", "k"], + spec=schedule_spec ) sched = scheduler.schedule() From 9dcebc32e835e00942bab14ab7000f06939b930e Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 1 Jun 2026 15:30:57 +0200 Subject: [PATCH 17/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 58 ++++++++++++++++++++-- src/xtc/csrcs/runtimes/host/perf_metrics.c | 4 +- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index c5e06dc3..f931a747 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -484,7 +484,16 @@ def _(mo, sandbox_form): Curious about branch misses or raw L1 cache loads? Write your own list of PMU/TMA events below. - *Temporary TMA metrics must be evaluate one at time without be mix up with PMU* + > **/!\\ Important Hardware Limits:** + + > CPUs only has a few programmable counters (usually 4 or 8). + + > * TMA metrics (like `TopdownL1`) consume up to 5 counters at once! + + > * If you ask for too many events, the CPU will run out of hardware counters. Linux will silently disable the overflowing ones. + + > * **Symptom:** You will see mathematically aberrant data, such as `[-0.0, -0.0]` or an absurdly high ones.If you see this, shorten your list! + *This cell will not run automatically. You must click the button to evaluate the kernel.* """), @@ -492,7 +501,6 @@ def _(mo, sandbox_form): ]) return - @app.cell def _(mo, module, sandbox_form): mo.stop(sandbox_form.value is None, mo.md("*Click 'Run Custom Metrics' to see the results.*")) @@ -516,16 +524,56 @@ def _(mo, module, sandbox_form): output_lines.append(f"**Terminal Output / Errors:**\n```text\n{err_sb}\n```") output_lines.append("**Raw Results:**\n```text") - for c, v in zip(custom_counters, results_sb): - output_lines.append(f"{c:35} : {v}") + + 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 + "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 + } + current_idx = 0 + hw_limit_exceeded = False + + for c in custom_counters: + size = DERIVED_METRICS_SIZES.get(c, 1) + chunk = results_sb[current_idx : current_idx + size] + + if size == 1: + output_lines.append(f"{c:35} : {int(chunk[0])}") + else: + rounded_chunk = [round(x, 2) for x in chunk] + output_lines.append(f"{c:35} : {rounded_chunk} (%)") + + # Missing hardware counters detection + if rounded_chunk == [0.0, 0.0, 0.0, 100.0] or all(x == 0.0 for x in rounded_chunk): + hw_limit_exceeded = True + + current_idx += size + output_lines.append("```") + if hw_limit_exceeded: + alert_msg = ( + " **Hardware Counter Limit Exceeded!**\n\n" + "Your CPU only has a limited number of programmable counters (usually 4 or 8). " + "You requested too many events at the same time. The Linux kernel failed to schedule the TMA events, " + "returning zeros (which mathematically defaults to 100% Backend Bound).\n\n" + "**Fix:** Remove some PMU counters from your list to make room for TMA." + ) + output_lines.append(mo.callout(mo.md(alert_msg), kind="danger")._repr_html_()) + sandbox_output = mo.md("\n".join(output_lines)) except Exception as e: sandbox_output = mo.md(f"**Error parsing your list:** {e}") - return custom_counters, evaluator_sb, output_lines, results_sb, sandbox_output + return custom_counters, evaluator_sb, hw_limit_exceeded, output_lines, results_sb, sandbox_output + + + + @app.cell diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 41e86b26..ccc1d3f3 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -437,8 +437,8 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 0; } - fprintf(stderr,"[DEBUG] Unsuported hardware\n"); - return 0; // Unsuported hardware + // Unsuported hardware / metric or the event is a pmu + return 0; } From 332b35e183f93bf6f6e81a03d353358bb6afb2b0 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 1 Jun 2026 16:36:51 +0200 Subject: [PATCH 18/92] [TMA]multiplexing implementation in progress --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 178 ++++++++++-------- .../csrcs/runtimes/host/perf_event_linux.c | 14 ++ src/xtc/csrcs/runtimes/host/perf_metrics.c | 97 ++++++++++ src/xtc/csrcs/runtimes/host/perf_metrics.h | 7 +- 4 files changed, 211 insertions(+), 85 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index 68f11939..163879fb 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -195,111 +195,123 @@ 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) { + // No event only time + if (events_num == 0) { + switch (nargs) { + case 0: evaluate0_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func0_t)func); break; + case 1: evaluate1_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; + case 2: evaluate2_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; + case 3: evaluate3_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; + case 4: evaluate4_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; + case 5: evaluate5_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func5_t)func, args[0], args[1], args[2], args[3], args[4]); break; + case 6: evaluate6_perf(results, 0, NULL, 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; + } + return; + } - metric_resolver_t resolver; + int max_passes = 1; int total_hw_events = 0; int total_out_results = 0; - int has_derived = 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; - metric_map_t *map = NULL; - int hw_events_num = events_num; - const char **hw_events_names = (const char **)events_names; - double *hw_results = results; + if (resolve_metric(events_names[i], &map[i].resolver) && map[i].resolver.is_supported) { + map[i].is_derived = 1; + if (map[i].resolver.num_passes > 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 { + map[i].is_derived = 0; + total_hw_events += 1; + total_out_results += 1; + } + } - if (events_num > 0) { - map = (metric_map_t *)malloc(events_num * sizeof(metric_map_t)); + double *hw_results = (double *)malloc(repeat * total_hw_events * sizeof(double)); + + // Multi-pass + for (int pass = 0; pass < max_passes; pass++) { + 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++) { - map[i].hw_offset = total_hw_events; - map[i].out_offset = total_out_results; - - if (resolve_metric(events_names[i], &map[i].resolver)) { - if (!map[i].resolver.is_supported) { - map[i].is_derived = 0; - total_hw_events += 1; - total_out_results += 1; - } else { - map[i].is_derived = 1; - has_derived = 1; - total_hw_events += map[i].resolver.num_hw_events; - total_out_results += map[i].resolver.num_results; + if (map[i].is_derived) { + if (pass < map[i].resolver.num_passes) { + int ev_start = 0; + for (int p = 0; p < pass; p++) ev_start += map[i].resolver.events_per_pass[p]; + int ev_count = map[i].resolver.events_per_pass[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 { - map[i].is_derived = 0; - total_hw_events += 1; - total_out_results += 1; + if (pass == 0) { // PMUs in the first pass + pass_events_names[pass_events_num] = events_names[i]; + pass_hw_offsets[pass_events_num] = map[i].hw_offset; + pass_events_num++; + } } } - } - if (has_derived) { - hw_events_names = (const char **)malloc(total_hw_events * sizeof(char *)); - int hw_idx = 0; + if (pass_events_num > 0) { + double *pass_results = (double *)malloc(repeat * pass_events_num * sizeof(double)); - for (int i = 0; i < events_num; i++) { - if (map[i].is_derived) { - for (int j = 0; j < map[i].resolver.num_hw_events; j++) { - hw_events_names[hw_idx++] = map[i].resolver.hw_events[j]; + switch (nargs) { + case 0: evaluate0_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func0_t)func); break; + case 1: evaluate1_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; + case 2: evaluate2_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; + case 3: evaluate3_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; + case 4: evaluate4_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; + case 5: evaluate5_perf(pass_results, pass_events_num, pass_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(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); break; + } + + // Gath 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]; } - } else { - hw_events_names[hw_idx++] = events_names[i]; } + free(pass_results); } - hw_results = (double *)malloc(repeat * total_hw_events * sizeof(double)); - } else { - total_hw_events = events_num; + free(pass_events_names); + free(pass_hw_offsets); } - switch (nargs) { - case 0: - evaluate0_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func0_t)func); - break; - case 1: - evaluate1_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); - break; - case 2: - evaluate2_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); - break; - case 3: - evaluate3_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); - break; - case 4: - evaluate4_perf(hw_results, total_hw_events, hw_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); - break; - case 5: - evaluate5_perf(hw_results, total_hw_events, hw_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(hw_results, total_hw_events, hw_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; - } - if (has_derived) { - 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) { - // Sécurité : si le noyau Linux a refusé d'ouvrir l'événement matériel (dépassement limite) - if (run_raw[0] == -1.0) { - 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 { - // Copie simple pour les PMU normaux - run_final[0] = run_raw[0]; + // 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_events_names); - free(hw_results); } + } + + free(hw_results); + free(map); } void evaluate(double *results, diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index a56721bd..23aebb08 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -238,6 +238,20 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.event = 0x02c2; else if (strcmp(name, "@skl_recovery") == 0) event->args.config_pair.event = 0x0100019d; + else if (strcmp(name, "@skl_mem_stalls") == 0) + event->args.config_pair.event = 0x1414; + else if (strcmp(name, "@skl_core_stalls") == 0) + event->args.config_pair.event = 0x01a2; + else if (strcmp(name, "@skl_fetch_lat_data") == 0) + event->args.config_pair.event = 0x0480; + else if (strcmp(name, "@skl_fetch_lat_tag") == 0) + event->args.config_pair.event = 0x0183; + else if (strcmp(name, "@skl_heavy_ops") == 0) + event->args.config_pair.event = 0x3079; + else if (strcmp(name, "@skl_br_misp") == 0) + event->args.config_pair.event = 0x00c5; + else if (strcmp(name, "@skl_machine_clears") == 0) + event->args.config_pair.event = 0x01c3; else return 1; diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index ccc1d3f3..91623d65 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -192,6 +192,16 @@ int detect_if_arm(void) { return ARCH_IS_ARM; } + +static const int pass_1[] = {1}; +static const int pass_4[] = {4}; +static const int pass_5[] = {5}; +static const int pass_9[] = {9}; + + +// === Skylake arch === + + static const char *skl_tma_l1_events[] = { "@skl_slots", // 0x003c (Cycles) "@skl_fe_bound", // 0x019c @@ -225,6 +235,85 @@ static void compute_skl_tma_l1(const double *raw_values, double *final_results) } } +static const int skl_l2_passes[] = {4, 4, 4}; // 3 passes of 4 counters +static const char *skl_tma_l2_events[] = { + // Passe 1 : L1 Base + Backend + "@skl_slots", // Fixe (0) + "cycle_activity:stalls_mem_any", // GP 1 (Memory Bound) + "resource_stalls:any", // GP 2 (Core Bound base) + "@skl_fe_bound", // GP 3 + + // Passe 2 : Frontend + "@skl_slots", // Fixe (4) + "icache_16b:ifdata_stall", // GP 1 (Fetch Latency) + "icache_64b:iftag_stall", // GP 2 (Fetch Latency) + "@skl_retiring", // GP 3 + + // Passe 3 : Retiring + Bad Speculation + "@skl_slots", // Fixe (8) + "idq:ms_uops", // GP 1 (Heavy Ops) + "br_misp_retired:all_branches", // GP 2 (Branch Mispredicts) + "machine_clears:count" // GP 3 (Machine Clears) +}; + + +static void compute_skl_tma_l2(const double *raw, double *final) { + // Passe 1 : 0 (slots), 1 (mem), 2 (core), 3 (fe) + // Passe 2 : 4 (slots), 5 (lat_d), 6 (lat_t), 7 (ret) + // Passe 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) { + // 1. Frontend + double fe_bound = raw[3] / slots_p1; + double fetch_lat = (raw[5] + raw[6]) / slots_p2; + double fetch_bw = fe_bound - fetch_lat; + + // 2. Retiring + double retiring = raw[7] / slots_p2; + double heavy_ops = raw[9] / slots_p3; + double light_ops = retiring - heavy_ops; + + // 3. Bad Speculation + double br_misp = raw[10] / slots_p3; + double m_clears = raw[11] / slots_p3; + double bad_spec = br_misp + m_clears; + + // 4. 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; + } +} + +// === Modern Inter arch === + static const char *intel_modern_tma_l1_events[] = { "@icl_slots", // 0x0400 (Group Leader) "@icl_retiring", // 0x8000 @@ -300,6 +389,8 @@ static void compute_intel_modern_tma_l2(const double *raw, double *final) { } } +// === Zen arch === + static const char *amd_zen4_tma_l1_events[] = { "@zen4_cyc", // 0x0076 (Cycles) "@zen4_fe", // 0x1000001A0 (Dispatch slots empty because frontend is stalled) @@ -344,6 +435,12 @@ 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 } + + + +// === Core logic === + + /* * Tested TopdownL1 : * - INTEL_SKYLAKE_CASCADE (skylake) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.h b/src/xtc/csrcs/runtimes/host/perf_metrics.h index f12754ce..ea12c18f 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.h +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.h @@ -7,10 +7,13 @@ typedef struct { int is_supported; - int num_hw_events; - const char **hw_events; 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; From a378b94aeb55352f5e65b8f6e466dece4e7558c5 Mon Sep 17 00:00:00 2001 From: subel Date: Tue, 2 Jun 2026 09:00:37 +0200 Subject: [PATCH 19/92] [TMA]add multi-pass to the skylake resolver --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 91623d65..71259af8 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -469,6 +469,8 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->num_hw_events = 5; out_resolver->hw_events = skl_tma_l1_events; out_resolver->num_results = 4; + out_resolver->num_passes = 1; + out_resolver->events_per_pass = pass_5; out_resolver->compute_formula = compute_skl_tma_l1; return 1; } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { @@ -477,6 +479,8 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->num_hw_events = 5; out_resolver->hw_events = intel_modern_tma_l1_events; out_resolver->num_results = 4; + out_resolver->num_passes = 1; + out_resolver->events_per_pass = pass_5; out_resolver->compute_formula = compute_intel_modern_tma_l1; return 1; } @@ -490,6 +494,8 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->num_hw_events = 4; out_resolver->hw_events = amd_zen4_tma_l1_events; out_resolver->num_results = 4; + out_resolver->num_passes = 1; + out_resolver->events_per_pass = pass_4; out_resolver->compute_formula = compute_amd_zen34_tma_l1; return 1; } @@ -497,9 +503,11 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 4; - out_resolver->hw_events = amd_zen4_tma_l1_events; + out_resolver->hw_events = amd_zen4_tma_l1_events; // Todo change to zen1 out_resolver->num_results = 4; - out_resolver->compute_formula = compute_amd_zen34_tma_l1; //compute_amd_zen1_tma_l1; + out_resolver->num_passes = 1; + out_resolver->events_per_pass = pass_4; + out_resolver->compute_formula = compute_amd_zen34_tma_l1; // Todo change to zen1 return 1; } // else if (uarch == AMD_ZEN_3) ... @@ -507,9 +515,6 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { else if (detect_if_arm()) { fprintf(stderr,"[DEBUG] ARM detected\n"); // todo - // fopen /sys/devices/system/cpu/cpu0/regs/identification/midr_el1 - // 0x410fd0c0 == Cortex-X1 - // 0x610f2200 == Apple M1 return 0; } } @@ -518,15 +523,23 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { intel_arch_t uarch = detect_intel_microarchitecture(); if (uarch == INTEL_SKYLAKE_CASCADE) { - fprintf(stderr,"[DEBUG] Unsuported INTEL_SKYLAKE_CASCADE for L2\n"); - // todo : need multiplexing - return 0; + fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L2 (Multi-Pass)\n"); + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 12; + out_resolver->hw_events = skl_tma_l2_events; + out_resolver->num_results = 8; + out_resolver->num_passes = 3; + out_resolver->events_per_pass = skl_l2_passes; // Tableau {4, 4, 4} + out_resolver->compute_formula = compute_skl_tma_l2; + return 1; } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { out_resolver->is_supported = 1; out_resolver->num_hw_events = 9; out_resolver->hw_events = intel_modern_tma_l2_events; out_resolver->num_results = 8; + out_resolver->num_passes = 1; + out_resolver->events_per_pass = pass_9; out_resolver->compute_formula = compute_intel_modern_tma_l2; return 1; } @@ -538,7 +551,6 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 0; } - int get_perf_metric_results_count(const char *metric_name) { metric_resolver_t resolver; if (resolve_metric(metric_name, &resolver) && resolver.is_supported) { From b90659195c34d8388b84e99b5b4833599ac51c4c Mon Sep 17 00:00:00 2001 From: subel Date: Tue, 2 Jun 2026 16:30:48 +0200 Subject: [PATCH 20/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 63 ++++++++++++---------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index f931a747..10438cd1 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -71,8 +71,8 @@ def _(mo): @app.cell def _(mo): _editor_code = '''import xtc.graphs.xtc.op as O -from xtc.backends.mlir import Backend -from xtc.schedules.descript import descript_scheduler + from xtc.backends.mlir import Backend + from xtc.schedules.descript import descript_scheduler # Problem setup I, J, K, dtype = 1024, 2048, 4096, "float32" @@ -138,7 +138,7 @@ def _trigger_compile(_): kind="success", on_click=_trigger_compile ) - return compile_btn, compile_params, set_compile_params + return compile_btn, compile_params @app.cell @@ -163,7 +163,6 @@ def _(compile_params, mo): module = local_vars.get("module") except Exception as e: exec_error = str(e) - return exec_error, module @@ -175,12 +174,12 @@ def _(compile_btn, mo, os, sys, tile_i_ui, tile_j_ui, unroll_ui): for i in range(4): # Usually index0 to index3 (L1d, L1i, L2, L3) path = f"/sys/devices/system/cpu/cpu0/cache/index{i}" if os.path.exists(path): - with open(f"{path}/level", "r") as f: - level = int(f.read().strip()) - with open(f"{path}/type", "r") as f: - ctype = f.read().strip() - with open(f"{path}/size", "r") as f: - size_str = f.read().strip() + with open(f"{path}/level", "r") as _f: + level = int(_f.read().strip()) + with open(f"{path}/type", "r") as _f: + ctype = _f.read().strip() + with open(f"{path}/size", "r") as _f: + size_str = _f.read().strip() s_kb = 0 if size_str.endswith('K'): s_kb = int(size_str[:-1]) @@ -279,11 +278,25 @@ def _(exec_error, mo, module, platform): else: pmu_counters = ["cycles", "instructions"] - if platform == "linux": - pmu_counters += [ - "mem_load_retired.l1_miss", - "mem_load_retired.l2_miss" - ] + if sys.platform == "linux": + is_amd = False + try: + with open("/proc/cpuinfo", "r") as f: + if "AuthenticAMD" in f.read(): + is_amd = True + except Exception: + pass + + if is_amd: + pmu_counters += [ + "all_l1_data_cache_fills", + "all_l2_cache_misses" + ] + else: + pmu_counters += [ + "mem_load_retired.l1_miss", + "mem_load_retired.l2_miss" + ] evaluator_pmu = module.get_evaluator( validate=True, @@ -296,7 +309,7 @@ def _(exec_error, mo, module, platform): mo.md(f"**Execution Code:** `{code_pmu}`"), mo.ui.table(_pmu_data, label="Raw PMU Results") ]) - return evaluator_pmu, pmu_counters, pmu_ui, results_pmu + return (pmu_ui,) @app.cell @@ -346,7 +359,7 @@ def _(exec_error, mo, module, platform): mo.md(f"**Execution Code:** `{code_l1}`"), _l1_ui_table ]) - return code_l1, error_l1, evaluator_l1, l1_ui, results_l1, tma_l1_counters + return (l1_ui,) @app.cell @@ -405,7 +418,7 @@ def _(exec_error, mo, module, platform): mo.md(f"**Execution Code:** `{code_l2}`"), _l2_ui_table ]) - return code_l2, error_l2, evaluator_l2, l2_ui, results_l2, tma_l2_counters + return (l2_ui,) @app.cell @@ -465,14 +478,14 @@ def _(mo): "branch-misses", "L1-dcache-loads", "L1-dcache-load-misses" -]''' + ]''' sandbox_editor = mo.ui.code_editor( value=_sandbox_default, language="python", ) sandbox_form = sandbox_editor.form(submit_button_label=" Run Custom Metrics") - return sandbox_editor, sandbox_form + return (sandbox_form,) @app.cell @@ -501,6 +514,7 @@ def _(mo, sandbox_form): ]) return + @app.cell def _(mo, module, sandbox_form): mo.stop(sandbox_form.value is None, mo.md("*Click 'Run Custom Metrics' to see the results.*")) @@ -526,7 +540,7 @@ def _(mo, module, sandbox_form): output_lines.append("**Raw Results:**\n```text") 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 + "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 "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 @@ -568,12 +582,7 @@ def _(mo, module, sandbox_form): except Exception as e: sandbox_output = mo.md(f"**Error parsing your list:** {e}") - - return custom_counters, evaluator_sb, hw_limit_exceeded, output_lines, results_sb, sandbox_output - - - - + return (sandbox_output,) @app.cell From bb27b78230777d82415df45d0b55f9354bfb1547 Mon Sep 17 00:00:00 2001 From: subel Date: Tue, 2 Jun 2026 16:32:49 +0200 Subject: [PATCH 21/92] [TMA]Zen4 support for TopdownL1 and TopdownL2 --- .../csrcs/runtimes/host/perf_event_linux.c | 70 ++++++--- src/xtc/csrcs/runtimes/host/perf_metrics.c | 136 ++++++++++++++---- src/xtc/utils/evaluation.py | 103 +++++++++---- 3 files changed, 241 insertions(+), 68 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 23aebb08..78c32a51 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -216,6 +216,7 @@ void stop_perf_events(int n_events, const int *fds, uint64_t *results) { * Source * Intel : https://github.com/torvalds/linux/blob/master/arch/x86/events/intel/core.c * AMD : https://github.com/torvalds/linux/blob/master/arch/x86/events/amd/core.c + * tools/perf/pmu-events/arch/x86/amdzen4/pipeline.json * * https://github.com/intel/perfmon * @@ -262,6 +263,7 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->mode = PERF_ARG_GENERIC; event->args.config_pair.type = PERF_TYPE_RAW; + // L1 if (strcmp(name, "@zen4_cyc") == 0) event->args.config_pair.event = 0x0076; else if (strcmp(name, "@zen4_fe") == 0) @@ -270,32 +272,62 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.event = 0x07AA; else if (strcmp(name, "@zen4_ret") == 0) event->args.config_pair.event = 0x00C1; + + // L2 + else if (strcmp(name, "@zen4_be_mem") == 0) + event->args.config_pair.event = 0x1000002A9ULL; + else if (strcmp(name, "@zen4_be_cpu") == 0) + event->args.config_pair.event = 0x1000004A9ULL; + + // L2 Frontend (cmask=0x6 add 0x06000000) + else if (strcmp(name, "@zen4_fe_lat") == 0) + event->args.config_pair.event = 0x1060001A9ULL; + else if (strcmp(name, "@zen4_fe_tot") == 0) + event->args.config_pair.event = 0x1000001A9ULL; + + // L2 Bad Speculation + Retiring + else if (strcmp(name, "@zen4_bs_misp") == 0) + event->args.config_pair.event = 0x0008000C1ULL; + else if (strcmp(name, "@zen4_bs_resync") == 0) + event->args.config_pair.event = 0x0002000C1ULL; + else if (strcmp(name, "@zen4_ret_micro") == 0) + event->args.config_pair.event = 0x0001000C1ULL; else - return 1; // unknow event + return 1; return 0; } // Modern Intel else if (strncmp(name, "@icl_", 5) == 0) { - event->mode = PERF_ARG_GENERIC; - event->args.config_pair.type = PERF_TYPE_RAW; - - if (strcmp(name, "@icl_slots") == 0) event->args.config_pair.event = 0x0400; - // TopDownL1 - else if (strcmp(name, "@icl_retiring") == 0) event->args.config_pair.event = 0x8000; - else if (strcmp(name, "@icl_bad_spec") == 0) event->args.config_pair.event = 0x8100; - else if (strcmp(name, "@icl_fe_bound") == 0) event->args.config_pair.event = 0x8200; - else if (strcmp(name, "@icl_be_bound") == 0) event->args.config_pair.event = 0x8300; - // TopDownL2 - else if (strcmp(name, "@icl_heavy_ops") == 0) event->args.config_pair.event = 0x8400; - else if (strcmp(name, "@icl_br_mispredict") == 0) event->args.config_pair.event = 0x8500; - else if (strcmp(name, "@icl_fetch_lat") == 0) event->args.config_pair.event = 0x8600; - else if (strcmp(name, "@icl_mem_bound") == 0) event->args.config_pair.event = 0x8700; - else return 1; - - return 0; - } + event->mode = PERF_ARG_GENERIC; + event->args.config_pair.type = PERF_TYPE_RAW; + + if (strcmp(name, "@icl_slots") == 0) + event->args.config_pair.event = 0x0400; + // TopDownL1 + else if (strcmp(name, "@icl_retiring") == 0) + event->args.config_pair.event = 0x8000; + else if (strcmp(name, "@icl_bad_spec") == 0) + event->args.config_pair.event = 0x8100; + else if (strcmp(name, "@icl_fe_bound") == 0) + event->args.config_pair.event = 0x8200; + else if (strcmp(name, "@icl_be_bound") == 0) + event->args.config_pair.event = 0x8300; + // TopDownL2 + else if (strcmp(name, "@icl_heavy_ops") == 0) + event->args.config_pair.event = 0x8400; + else if (strcmp(name, "@icl_br_mispredict") == 0) + event->args.config_pair.event = 0x8500; + else if (strcmp(name, "@icl_fetch_lat") == 0) + event->args.config_pair.event = 0x8600; + else if (strcmp(name, "@icl_mem_bound") == 0) + event->args.config_pair.event = 0x8700; + else + return 1; + + return 0; + } return -1; } diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 71259af8..1bc717ac 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -238,22 +238,22 @@ static void compute_skl_tma_l1(const double *raw_values, double *final_results) static const int skl_l2_passes[] = {4, 4, 4}; // 3 passes of 4 counters static const char *skl_tma_l2_events[] = { // Passe 1 : L1 Base + Backend - "@skl_slots", // Fixe (0) - "cycle_activity:stalls_mem_any", // GP 1 (Memory Bound) - "resource_stalls:any", // GP 2 (Core Bound base) - "@skl_fe_bound", // GP 3 + "@skl_slots", + "cycle_activity:stalls_mem_any", // Memory Bound + "resource_stalls:any", // Core Bound base + "@skl_fe_bound", // Passe 2 : Frontend - "@skl_slots", // Fixe (4) - "icache_16b:ifdata_stall", // GP 1 (Fetch Latency) - "icache_64b:iftag_stall", // GP 2 (Fetch Latency) - "@skl_retiring", // GP 3 + "@skl_slots", + "icache_16b:ifdata_stall", // Fetch Latency + "icache_64b:iftag_stall", // Fetch Latency + "@skl_retiring", // Passe 3 : Retiring + Bad Speculation - "@skl_slots", // Fixe (8) - "idq:ms_uops", // GP 1 (Heavy Ops) - "br_misp_retired:all_branches", // GP 2 (Branch Mispredicts) - "machine_clears:count" // GP 3 (Machine Clears) + "@skl_slots", + "idq:ms_uops", // Heavy Ops + "br_misp_retired:all_branches", // Branch Mispredicts + "machine_clears:count" // Machine Clears }; @@ -435,24 +435,91 @@ 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; + + // 3. Bad Speculation + double bs_misp = raw[6] / slots_p2; + double bs_clear = raw[7] / slots_p2; + + // 4. 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; + } +} // === Core logic === +/* AMD metrics + * backend_bound_memory = Memory bound + * backend_bound_cpu = Core Bound + * frontend_bound_latency = Fetch Latency + * frontend_bound_bw = Bandwidth + * bad_speculation_mispredicts = Branch Mispredicts + */ + + + + /* * Tested TopdownL1 : - * - INTEL_SKYLAKE_CASCADE (skylake) - * - INTEL_ICELAKE_SAPPHIRE (raptor lake) + * - INTEL_SKYLAKE_CASCADE (skylake) L1,L2 + * - INTEL_ICELAKE_SAPPHIRE (raptor lake) L1,L2 + * - AMD_ZEN_4 L1,L2 * * Untested : * - AMD_ZEN_1_2 - * - AMD_ZEN_4 * * Not implemented : * - Aarch64 * - Zen 3 + * - L3 support * * */ @@ -500,26 +567,29 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } else if (uarch == AMD_ZEN_1_2) { - fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 4; - out_resolver->hw_events = amd_zen4_tma_l1_events; // Todo change to zen1 - out_resolver->num_results = 4; - out_resolver->num_passes = 1; - out_resolver->events_per_pass = pass_4; - out_resolver->compute_formula = compute_amd_zen34_tma_l1; // Todo change to zen1 - return 1; + fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected (unsuported)\n"); + //out_resolver->is_supported = 1; + //out_resolver->num_hw_events = 4; + //out_resolver->hw_events = amd_zen4_tma_l1_events; // Todo change to zen1 + //out_resolver->num_results = 4; + //out_resolver->num_passes = 1; + //out_resolver->events_per_pass = pass_4; + //out_resolver->compute_formula = compute_amd_zen34_tma_l1; // Todo change to zen1 + return 0; } // else if (uarch == AMD_ZEN_3) ... } else if (detect_if_arm()) { fprintf(stderr,"[DEBUG] ARM detected\n"); // todo + // fopen /sys/devices/system/cpu/cpu0/regs/identification/midr_el1 + // 0x410fd0c0 == Cortex-X1 + // 0x610f2200 == Apple M1 return 0; } } else if (strcmp(metric_name, "TopdownL2") == 0) { - if (detect_if_intel()) { + if (detect_if_intel()) { // L2 intel intel_arch_t uarch = detect_intel_microarchitecture(); if (uarch == INTEL_SKYLAKE_CASCADE) { @@ -534,6 +604,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { + fprintf(stderr,"[DEBUG] INTEL_ICELAKE_SAPPHIRE L2\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 9; out_resolver->hw_events = intel_modern_tma_l2_events; @@ -544,6 +615,21 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } } + + else if (detect_if_amd()) { // L2 amd + amd_arch_t uarch = detect_amd_microarchitecture(); + if (uarch == AMD_ZEN_4) { + fprintf(stderr,"[DEBUG] AMD_ZEN_4 L2 (Multi-Pass)\n"); + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 9; + out_resolver->hw_events = amd_zen4_tma_l2_events; + out_resolver->num_results = 8; + out_resolver->num_passes = 2; + out_resolver->events_per_pass = amd_zen4_l2_passes; + out_resolver->compute_formula = compute_amd_zen4_tma_l2; + return 1; + } + } return 0; } diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index 21d1419d..a0a56e9c 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -2,24 +2,26 @@ # 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 numpy as np -import subprocess -import shutil import os +import shutil import signal +import subprocess +import sys import time -from typing import Callable, Any -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 typing import Any, Callable, cast + +import numpy as np + 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] = [] @@ -151,16 +153,23 @@ 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 - "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 + "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 + "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 evaluate_performance( func: Callable[[Any], Any], parameters: tuple[list[NDArray], list[NDArray]], @@ -215,7 +224,7 @@ def evaluate_performance( args_array, len(args_array), ) - print(f"results: {[round(x, 2) for x in results_array]}") + print(f"[DEBUG] results: {[round(x, 2) for x in results_array]}") eval_results = [float(x) for x in results_array] failed_counters = [] @@ -232,7 +241,9 @@ def evaluate_performance( # Fallback on linux perf tool if failed_counters: - print(f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'...") + print( + f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'..." + ) perf_path = shutil.which("perf") if not perf_path: @@ -241,7 +252,28 @@ def evaluate_performance( 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] + perf_events = [c for c in failed_counters if c not in DERIVED_METRICS_SIZES] + + # amd have no native TopdownL2 but we can reconstruct it with right metrics + is_amd = False + if sys.platform == "linux": + try: + with open("/proc/cpuinfo", "r") as f: + if "AuthenticAMD" in f.read(): + is_amd = True + except: + 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] @@ -252,7 +284,9 @@ def evaluate_performance( try: print("[DEBUG] Starting perf...") - perf_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + perf_proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) time.sleep(1.5) @@ -260,10 +294,29 @@ def evaluate_performance( dummy_results = (ctypes.c_double * repeat)() if cfunc.is_packed: print("[DEBUG] Rerun packed...") - _ = runtime.evaluate_packed_perf(dummy_results, [], repeat, number, min_repeat_ms, cfunc, args_array_packed, args_codes_packed, len(args_tuples)) + _ = runtime.evaluate_packed_perf( + dummy_results, + [], + repeat, + number, + min_repeat_ms, + cfunc, + args_array_packed, + args_codes_packed, + len(args_tuples), + ) else: print("[DEBUG] Rerun not packed...") - runtime.evaluate_perf(dummy_results, [], repeat, number, min_repeat_ms, cfunc, args_array, len(args_array)) + runtime.evaluate_perf( + dummy_results, + [], + repeat, + number, + min_repeat_ms, + cfunc, + args_array, + len(args_array), + ) print("[DEBUG] Stopping perf...") perf_proc.send_signal(signal.SIGINT) @@ -272,7 +325,9 @@ def evaluate_performance( 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( + f"\n====== Fallback 'perf stat' Output for {failed_counters} ======\n" + ) print(stderr_output) print("===================================\n") From dae5cbe4357027f9a2cb74455e69cce0c8058edf Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 3 Jun 2026 11:43:23 +0200 Subject: [PATCH 22/92] [TMA]allow to ask multiples tma without overload HW counters registers --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 128 +++++++++++--------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index 163879fb..10e73905 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -189,6 +189,7 @@ typedef struct { metric_resolver_t resolver; int hw_offset; int out_offset; + int start_pass; } metric_map_t; void evaluate_perf(double *results, int events_num, const char *events_names[], @@ -213,78 +214,91 @@ void evaluate_perf(double *results, int events_num, const char *events_names[], 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; - if (map[i].resolver.num_passes > 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 { - map[i].is_derived = 0; - total_hw_events += 1; - total_out_results += 1; - } - } + map[i].hw_offset = total_hw_events; + map[i].out_offset = total_out_results; - double *hw_results = (double *)malloc(repeat * total_hw_events * sizeof(double)); + if (resolve_metric(events_names[i], &map[i].resolver) && map[i].resolver.is_supported) { + map[i].is_derived = 1; - // Multi-pass - for (int pass = 0; pass < max_passes; pass++) { - 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)); + // isolate the TMA within it own passes + map[i].start_pass = max_passes; + max_passes += map[i].resolver.num_passes; - for (int i = 0; i < events_num; i++) { - if (map[i].is_derived) { - if (pass < map[i].resolver.num_passes) { - int ev_start = 0; - for (int p = 0; p < pass; p++) ev_start += map[i].resolver.events_per_pass[p]; - int ev_count = map[i].resolver.events_per_pass[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++; - } - } + total_hw_events += map[i].resolver.num_hw_events; + total_out_results += map[i].resolver.num_results; } else { - if (pass == 0) { // PMUs in the first pass - pass_events_names[pass_events_num] = events_names[i]; - pass_hw_offsets[pass_events_num] = map[i].hw_offset; - pass_events_num++; - } + // 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; } } - if (pass_events_num > 0) { - double *pass_results = (double *)malloc(repeat * pass_events_num * sizeof(double)); - - switch (nargs) { - case 0: evaluate0_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func0_t)func); break; - case 1: evaluate1_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; - case 2: evaluate2_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; - case 3: evaluate3_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; - case 4: evaluate4_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; - case 5: evaluate5_perf(pass_results, pass_events_num, pass_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(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); break; + 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; // De 0 à N pour cette métrique + + 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++; + } + } } - // Gath 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]; + if (pass_events_num > 0) { + double *pass_results = (double *)malloc(repeat * pass_events_num * sizeof(double)); + + switch (nargs) { + case 0: evaluate0_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func0_t)func); break; + case 1: evaluate1_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; + case 2: evaluate2_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; + case 3: evaluate3_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; + case 4: evaluate4_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; + case 5: evaluate5_perf(pass_results, pass_events_num, pass_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(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); break; } + // 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_results); + free(pass_events_names); + free(pass_hw_offsets); } - free(pass_events_names); - free(pass_hw_offsets); - } // Postprocessing for (int r = 0; r < repeat; r++) { From 20ff0bf689e7da8eca30025bd7f0f30f183f104b Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 3 Jun 2026 11:44:23 +0200 Subject: [PATCH 23/92] [TMA]update TMA test --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 2 +- .../evaluation/test_matmul_tma_counters.py | 77 +++++++++++-------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index 10e73905..7516f02f 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -255,7 +255,7 @@ void evaluate_perf(double *results, int events_num, const char *events_names[], 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; // De 0 à N pour cette métrique + 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]; diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index ad40d207..4cd392a1 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -3,7 +3,7 @@ import xtc.graphs.xtc.op as O from xtc.backends.mlir import Backend -from sys import platform +import sys #I, J, K, dtype = 4, 32, 512, "float32" # small I, J, K, dtype = 1024, 2048, 4096, "float32" # medium @@ -36,11 +36,11 @@ hw_counters = [] # Linux Perf counters -if platform == "linux": +if sys.platform == "linux": hw_counters += [ - "TopdownL1" + "TopdownL1","TopdownL2" ] -elif platform == "darwin": +elif sys.platform == "darwin": # On MacOS, requires sudo to get counters # TODO: should be tested ideally hw_counters = [] @@ -51,34 +51,47 @@ pmu_counters=hw_counters, ) results, code, error = evaluator.evaluate() -print(f"CODE: {code}") -print(f"counters: {hw_counters}") -print(f"results TopDownL1: {[int(x) for x in results]}") - -print("=============\n") - -hw_counters = ["mem_load_retired.l1_miss","mem_load_retired.l2_miss","mem_load_retired.l3_miss"] - -# Linux Perf counters -if platform == "linux": - hw_counters += [ - "TopdownL2", - "TopdownL3" - ] -elif platform == "darwin": - # On MacOS, requires sudo to get counters - # TODO: should be tested ideally - hw_counters = [] - - -evaluator = module.get_evaluator( - validate=True, - pmu_counters=hw_counters, -) -results, code, error = evaluator.evaluate() -print(f"CODE: {code}") -print(f"counters: {hw_counters}") -print(f"results : {[int(x) for x in results]}") +print(f"{'CODE':<25}: {code}") +print(f"{'counters':<25}: {hw_counters}") +print(f"{'results':<25}: {[round(x, 2) for x in results]}") + + +use_colors = sys.stdout.isatty() + +RED = "\033[91m" if use_colors else "" +ORANGE = "\033[38;5;208m" if use_colors else "" +YELLOW = "\033[93m" if use_colors else "" +GREEN = "\033[92m" if use_colors else "" +MAGENTA = "\033[95m" if use_colors else "" +RESET = "\033[0m" if use_colors else "" + +def get_c(val): + if val > 90: return RED + if val > 75: return ORANGE + if val > 50: return YELLOW + if val > 10: return GREEN + if val == -1: return MAGENTA + return RESET + +w = 25 + +print("-" * (w + 10)) + +# L1 Metrics +print(f"{'L1 Retiring':<{w}}: {get_c(results[0])}{results[0]:.2f}{RESET}") +print(f"{'L1 Bad speculation':<{w}}: {get_c(results[1])}{results[1]:.2f}{RESET}") +print(f"{'L1 Frontend bound':<{w}}: {get_c(results[2])}{results[2]:.2f}{RESET}") +print(f"{'L1 Backend bound':<{w}}: {get_c(results[3])}{results[3]:.2f}{RESET}") + +print("") +# L2 Metrics +print(f"{'L2 Light ops':<{w}}: {get_c(results[4])}{results[4]:.2f}{RESET}") +print(f"{'L2 Heavy ops':<{w}}: {get_c(results[5])}{results[5]:.2f}{RESET}") +print(f"{'L2 Machine clear':<{w}}: {get_c(results[6])}{results[6]:.2f}{RESET}") +print(f"{'L2 Branch misspredict':<{w}}: {get_c(results[7])}{results[7]:.2f}{RESET}") +print(f"{'L2 Fetch latency':<{w}}: {get_c(results[8])}{results[8]:.2f}{RESET}") +print(f"{'L2 Core bound':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") +print(f"{'L2 Memory bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") # CHECK: CODE: 0 # CHECK-NEXT: counters: From 379cb70cb9b22cff593a4174ac9d4d223f34748e Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 3 Jun 2026 16:04:25 +0200 Subject: [PATCH 24/92] [TMA]opti : kernel is not runned if all events of the current TMA pass failed to be handled --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 41 ++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index 7516f02f..febd4a37 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -279,19 +279,42 @@ void evaluate_perf(double *results, int events_num, const char *events_names[], if (pass_events_num > 0) { double *pass_results = (double *)malloc(repeat * pass_events_num * sizeof(double)); - switch (nargs) { - case 0: evaluate0_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func0_t)func); break; - case 1: evaluate1_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; - case 2: evaluate2_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; - case 3: evaluate3_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; - case 4: evaluate4_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; - case 5: evaluate5_perf(pass_results, pass_events_num, pass_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(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); break; + 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_events_num); + // 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 { + switch (nargs) { + case 0: evaluate0_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func0_t)func); break; + case 1: evaluate1_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; + case 2: evaluate2_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; + case 3: evaluate3_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; + case 4: evaluate4_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; + case 5: evaluate5_perf(pass_results, pass_events_num, pass_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(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); break; + } } // 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]; + hw_results[r * total_hw_events + pass_hw_offsets[e]] = + pass_results[r * pass_events_num + e]; } } free(pass_results); From 576f5ceb781774a07b37e5c3e89cf41103c29c0c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 3 Jun 2026 16:04:57 +0200 Subject: [PATCH 25/92] [TMA]clean debug comments --- src/xtc/csrcs/runtimes/host/perf_event_linux.c | 4 ---- src/xtc/utils/evaluation.py | 10 ++++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 78c32a51..1828f890 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -333,7 +333,6 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) } int get_perf_event_config(const char *name, perf_event_args_t *event) { - printf("[DEBUG] full name : %s\n",name); for (int e = 0; e < sizeof(perf_events_decl) / sizeof(*perf_events_decl); e++) { if (strcmp(name, perf_events_decl[e].name) == 0) { @@ -370,9 +369,6 @@ int get_perf_event_config(const char *name, perf_event_args_t *event) { event->mode = PERF_ARG_PTR; event->args.config_ptr = (const void *)arg; return 0; - } else { - fprintf(stderr, "[DEBUG] libpfm4 unknow event '%s' : %s\n", name, - pfm_strerror(ret)); } free(arg); free(attr); diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index a0a56e9c..52c40304 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -241,9 +241,7 @@ def evaluate_performance( # Fallback on linux perf tool if failed_counters: - print( - f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'..." - ) + print(f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'...") perf_path = shutil.which("perf") if not perf_path: @@ -288,12 +286,12 @@ def evaluate_performance( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) - time.sleep(1.5) + time.sleep(0.75) # Rerun evaluation without HW counters to generate activity for perf dummy_results = (ctypes.c_double * repeat)() if cfunc.is_packed: - print("[DEBUG] Rerun packed...") + print("[DEBUG] Rerun packed (perf)...") _ = runtime.evaluate_packed_perf( dummy_results, [], @@ -306,7 +304,7 @@ def evaluate_performance( len(args_tuples), ) else: - print("[DEBUG] Rerun not packed...") + print("[DEBUG] Rerun not packed (perf)...") runtime.evaluate_perf( dummy_results, [], From 57c08942f12021453c572939478d73465578279c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 3 Jun 2026 16:05:21 +0200 Subject: [PATCH 26/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 336 ++++++++------------- 1 file changed, 128 insertions(+), 208 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 10438cd1..04234748 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -15,7 +15,7 @@ @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" # Hardware Performance Counters & Top-down Analysis @@ -29,7 +29,7 @@ def _(mo): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" > **Disclaimer and prerequisites:** > Results will vary depending on your hardware architecture (MacOS is currently not supported). @@ -45,23 +45,21 @@ def _(mo): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## 1. Defining the Workload & Schedule We define a medium-sized matrix multiplication (1024x2048x4096). - **Use the code editor below** to modify the scheduling specification using the `descript` notation. - **Interactive Workflow:** - 1. Adjust the sliders in the sidebar to see how the Cache Footprint updates instantly. - 2. Click the **"Compile & Evaluate"** button in the sidebar to apply the schedule and update the TMA metrics. + 1. Adjust the sliders in the sidebar to update the tile size. + 2. Click the evaluation buttons below each section to run the PMU/TMA counters on demand. """) return @app.cell -def _(mo): - # Interactive UI elements for the schedule +def _(): + # UI sliders tile_i_ui = mo.ui.slider(start=1, stop=64, step=1, value=3, label="Tile I (Rows)") tile_j_ui = mo.ui.slider(start=8, stop=512, step=8, value=24, label="Tile J (Cols)") unroll_ui = mo.ui.slider(start=1, stop=8, step=1, value=2, label="Unroll factor") @@ -69,7 +67,7 @@ def _(mo): @app.cell -def _(mo): +def _(): _editor_code = '''import xtc.graphs.xtc.op as O from xtc.backends.mlir import Backend from xtc.schedules.descript import descript_scheduler @@ -94,6 +92,7 @@ def _(mo): f"i#{slider_i}": {"vectorize": True} } + # Compile scheduler = backend.get_scheduler() descript_scheduler( scheduler=scheduler, @@ -121,65 +120,48 @@ def _(descript_editor): @app.cell -def _(descript_editor, mo, tile_i_ui, tile_j_ui, unroll_ui): - # State management to isolate heavy computation from slider movement - compile_params, set_compile_params = mo.state(None) - - def _trigger_compile(_): - set_compile_params({ - "code": descript_editor.value, - "i": tile_i_ui.value, - "j": tile_j_ui.value, - "unroll": unroll_ui.value - }) - - compile_btn = mo.ui.button( - label="Compile & Evaluate", - kind="success", - on_click=_trigger_compile - ) - return compile_btn, compile_params - - -@app.cell -def _(compile_params, mo): - mo.stop(compile_params() is None, mo.md("*👈 Click **'Compile & Evaluate'** in the sidebar to start.*")) - - _params = compile_params() +def _(descript_editor, tile_i_ui, tile_j_ui, unroll_ui): + # Execute user code safely local_vars = {} + # Inject slider values into execution context global_vars = globals().copy() global_vars.update({ - "slider_i": _params["i"], - "slider_j": _params["j"], - "slider_unroll": _params["unroll"], + "slider_i": tile_i_ui.value, + "slider_j": tile_j_ui.value, + "slider_unroll": unroll_ui.value, }) exec_error = None module = None + I_val, J_val, K_val = 1024, 2048, 4096 try: - exec(_params["code"], global_vars, local_vars) + exec(descript_editor.value, global_vars, local_vars) module = local_vars.get("module") + I_val = local_vars.get("I", 1024) + J_val = local_vars.get("J", 2048) + K_val = local_vars.get("K", 4096) except Exception as e: exec_error = str(e) - return exec_error, module + return I_val, J_val, K_val, exec_error, module @app.cell -def _(compile_btn, mo, os, sys, tile_i_ui, tile_j_ui, unroll_ui): +def _(I_val, J_val, K_val, tile_i_ui, tile_j_ui, unroll_ui): + # Fetch hardware cache sizes dynamically (sysfs) caches_kb = {} if sys.platform == "linux" and os.path.exists("/sys/devices/system/cpu/cpu0/cache"): try: - for i in range(4): # Usually index0 to index3 (L1d, L1i, L2, L3) + for i in range(4): path = f"/sys/devices/system/cpu/cpu0/cache/index{i}" if os.path.exists(path): - with open(f"{path}/level", "r") as _f: - level = int(_f.read().strip()) - with open(f"{path}/type", "r") as _f: - ctype = _f.read().strip() - with open(f"{path}/size", "r") as _f: - size_str = _f.read().strip() + with open(f"{path}/level", "r") as f: + level = int(f.read().strip()) + with open(f"{path}/type", "r") as f: + ctype = f.read().strip() + with open(f"{path}/size", "r") as f: + size_str = f.read().strip() s_kb = 0 if size_str.endswith('K'): s_kb = int(size_str[:-1]) @@ -192,14 +174,12 @@ def _(compile_btn, mo, os, sys, tile_i_ui, tile_j_ui, unroll_ui): except Exception: pass - # Fallbacks in case reading failed + # Defaults if reading fails if "L1d" not in caches_kb: caches_kb["L1d"] = 1 if "L2" not in caches_kb: caches_kb["L2"] = 1 if len(caches_kb) <= 2 and "L3" not in caches_kb: caches_kb["L3"] = 1 - _I, _J, _K = 1024, 2048, 4096 - b_size = 4 # float32 - # todo : check element size dynamicly + b_size = 4 def fmt(b): if b >= 1024**2: return f"{b / 1024**2:.1f} MiB" @@ -209,13 +189,13 @@ def fmt(b): geo_md = f""" | Tensor | Total Size | 1 Row | 1 Col | |---|---|---|---| - | **A** ({_I}×{_K}) | {fmt(_I*_K*b_size)} | {fmt(_K*b_size)} | {fmt(_I*b_size)} | - | **B** ({_K}×{_J}) | {fmt(_K*_J*b_size)} | {fmt(_J*b_size)} | {fmt(_K*b_size)} | - | **C** ({_I}×{_J}) | {fmt(_I*_J*b_size)} | {fmt(_J*b_size)} | {fmt(_I*b_size)} | + | **A** ({I_val}×{K_val}) | {fmt(I_val*K_val*b_size)} | {fmt(K_val*b_size)} | {fmt(I_val*b_size)} | + | **B** ({K_val}×{J_val}) | {fmt(K_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(K_val*b_size)} | + | **C** ({I_val}×{J_val}) | {fmt(I_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(I_val*b_size)} | """ tile_c_bytes = tile_i_ui.value * tile_j_ui.value * b_size - flops = 2 * _I * _J * _K + flops = 2 * I_val * J_val * K_val gflops = flops / 1e9 def format_cache(name, kb): @@ -234,8 +214,6 @@ def format_cache(name, kb): sidebar = mo.sidebar( mo.vstack([ - compile_btn, - mo.md("---"), mo.md("### Schedule Controls"), tile_i_ui, tile_j_ui, @@ -246,9 +224,9 @@ def format_cache(name, kb): mo.md(f"**Total Math:** `{gflops:.1f} GFLOPs`"), mo.md(f"**Current C-Tile (i×j):** `{fmt(tile_c_bytes)}`"), mo.md("---"), - mo.md("### Your CPU Caches"), - mo.md(cache_md), - mo.md("
*Compare row/col sizes to your caches to predict bottlenecks!*") + mo.md("### CPU Caches"), + mo.md("
*Compare row/col sizes to your caches to predict bottlenecks!*"), + mo.md(cache_md) ]) ) return (sidebar,) @@ -260,69 +238,63 @@ def _(sidebar): return +@app.cell +def _(): + # On-demand execution buttons + btn_pmu = mo.ui.run_button(kind="info", label="Evaluate Raw PMUs") + btn_l1 = mo.ui.run_button(kind="info", label="Evaluate Topdown L1") + btn_l2 = mo.ui.run_button(kind="info", label="Evaluate Topdown L2") + + _sandbox_default = '[\n "instructions",\n "branches",\n "branch-misses",\n "L1-dcache-loads",\n "L1-dcache-load-misses",\n "TopdownL1",\n "TopdownL2",\n "TopdownL3"\n]' + sandbox_editor = mo.ui.code_editor(value=_sandbox_default, language="python") + btn_sandbox = mo.ui.run_button(kind="success", label="Run Custom Metrics") + return btn_l1, btn_l2, btn_pmu, btn_sandbox, sandbox_editor + + @app.cell(hide_code=True) -def _(mo): - mo.md(r""" +def _(btn_pmu): + mo.md(f""" ## 2. Raw Hardware Counters (PMU) CPUs expose raw counters to track specific events. We can ask the CPU exactly how many cycles were spent or how many L1/L2 cache misses occurred. *Note: Raw event names are highly architecture-dependent. `libpfm4` helps translating them. The `perf list` command can show you available ones.* + + {btn_pmu} """) return @app.cell -def _(exec_error, mo, module, platform): +def _(btn_pmu, exec_error, module): + # Wait for button click + mo.stop(not btn_pmu.value, mo.md("*Click the button above to execute PMU Evaluation.*")) + if module is None: pmu_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: pmu_counters = ["cycles", "instructions"] + if platform == "linux": + pmu_counters += ["mem_load_retired.l1_miss", "mem_load_retired.l2_miss"] - if sys.platform == "linux": - is_amd = False - try: - with open("/proc/cpuinfo", "r") as f: - if "AuthenticAMD" in f.read(): - is_amd = True - except Exception: - pass - - if is_amd: - pmu_counters += [ - "all_l1_data_cache_fills", - "all_l2_cache_misses" - ] - else: - pmu_counters += [ - "mem_load_retired.l1_miss", - "mem_load_retired.l2_miss" - ] - - evaluator_pmu = module.get_evaluator( - validate=True, - pmu_counters=pmu_counters, - ) + evaluator_pmu = module.get_evaluator(validate=True, pmu_counters=pmu_counters) results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() - _pmu_data = [{"Counter": c, "Value": int(v)} for c, v in zip(pmu_counters, results_pmu)] + _pmu_data = [{"Counter": c, "Value": "Fallback needed" if v == -1.0 else str(int(v))} for c, v in zip(pmu_counters, results_pmu)] pmu_ui = mo.vstack([ mo.md(f"**Execution Code:** `{code_pmu}`"), mo.ui.table(_pmu_data, label="Raw PMU Results") ]) - return (pmu_ui,) - -@app.cell -def _(pmu_ui): + # Render UI pmu_ui return @app.cell(hide_code=True) -def _(mo): - mo.md(r""" +def _(btn_l1): + mo.md(f""" ## 3. Top-down Microarchitecture Analysis (Level 1) - Raw counters are hard to interpret: *Is 5 million cache misses bad? Does it actually stall the CPU?* + Raw counters are hard to interpret. To solve this, **TMA** (Top-down Analysis) groups all CPU pipeline slots into 4 distinct categories, summing up to 100%: To solve this, **TMA** (Top-down Analysis) groups all CPU pipeline slots into 4 distinct categories, summing up to 100%: * 🟢 **Retiring:** Good! The CPU is doing useful work (executing our math). @@ -334,7 +306,9 @@ def _(mo): @app.cell -def _(exec_error, mo, module, platform): +def _(btn_l1, exec_error, module): + mo.stop(not btn_l1.value, mo.md("*Click the button above to execute Topdown L1 Evaluation.*")) + if module is None: l1_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: @@ -342,13 +316,11 @@ def _(exec_error, mo, module, platform): evaluator_l1 = module.get_evaluator(validate=False, pmu_counters=tma_l1_counters) results_l1, code_l1, error_l1 = evaluator_l1.evaluate() - _l1_labels = ["Retiring", "Bad Speculation", "Frontend Bound", "Backend Bound"] + _l1_labels = ["🟢 Retiring", "🔴 Bad Speculation", "🔵 Frontend Bound", "🟣 Backend Bound"] if tma_l1_counters and len(results_l1) > 0: if results_l1[0] < 0: - _l1_ui_table = mo.accordion({ - "Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```") - }) + _l1_ui_table = mo.accordion({"Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```")}) else: _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") @@ -359,31 +331,29 @@ def _(exec_error, mo, module, platform): mo.md(f"**Execution Code:** `{code_l1}`"), _l1_ui_table ]) - return (l1_ui,) - -@app.cell -def _(l1_ui): l1_ui return @app.cell(hide_code=True) -def _(mo): - mo.md(r""" +def _(btn_l2): + mo.md(f""" ## 4. Drilling Down (Topdown Level 2) - If our code is heavily **Backend Bound**, we need to know why! Is it because our math is too complex for the ALU/FPU (`Core Bound`), or are we constantly waiting for the RAM (`Memory Bound`)? + If our code is heavily **Backend Bound**, we need to know why. TMA Level 2 splits the L1 categories further: - TMA Level 2 splits the L1 categories further. The Backend Bound is split into: + * **Core Bound:** Lack of execution units (ALU/FPU) or data dependencies between instructions. + * **Memory Bound:** Execution Units are starved because of non-completed in-flight memory demand loads. - * **Core Bound:** The pipeline is likely stalled due to a lack of available execution units (ALU/FPU) or data dependencies between instructions preventing out-of-order execution. - * **Memory Bound:** The pipeline is likely stalled due to demand load/store instructions. This represents the fraction of slots where Execution Units are starved because of non-completed in-flight memory demand loads, or occasionally when store buffers are completely full imposing backpressure. + {btn_l2} """) return @app.cell -def _(exec_error, mo, module, platform): +def _(btn_l2, exec_error, module): + mo.stop(not btn_l2.value, mo.md("*Click the button above to execute Topdown L2 Evaluation.*")) + if module is None: l2_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: @@ -405,8 +375,8 @@ def _(exec_error, mo, module, platform): if tma_l2_counters and len(results_l2) > 0: if results_l2[0] < 0: _l2_ui_table = mo.vstack([ - mo.callout(mo.md("⚠️ *Internal C resolver unsupported for L2. Showing `perf stat` output below.*"), kind="warn"), - mo.accordion({"Terminal Output from `perf`": mo.md(f"```text\n{error_l2}\n```")}) + mo.callout(mo.md("*Internal C resolver unsupported for L2. Showing perf stat output below.*"), kind="warn"), + mo.accordion({"Terminal Output from perf": mo.md(f"```text\n{error_l2}\n```")}) ]) else: _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] @@ -414,26 +384,17 @@ def _(exec_error, mo, module, platform): else: _l2_ui_table = mo.md("*TMA L2 is not supported on this machine.*") - l2_ui = mo.vstack([ - mo.md(f"**Execution Code:** `{code_l2}`"), - _l2_ui_table - ]) - return (l2_ui,) + l2_ui = mo.vstack([mo.md(f"**Execution Code:** `{code_l2}`"), _l2_ui_table]) - -@app.cell -def _(l2_ui): l2_ui return @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r""" ## How to fix Core Bound vs Memory Bound? - Once you know where your bottleneck is, you can adapt your code or your MLIR schedule: - ### If you are heavily Core Bound: Your CPU has all the data it needs in the caches, but it cannot crunch the numbers fast enough. * **Vectorization:** Ensure your loops are properly vectorized so the CPU processes 8 or 16 floats per instruction (AVX2/AVX-512) instead of 1. @@ -448,48 +409,7 @@ def _(mo): @app.cell(hide_code=True) -def _(mo): - mo.md(r""" - ## Going deeper: Topdown L3, L4 and beyond... - - Topdown Level 2 is great, but it sometimes leaves us with more questions. If you are 60% *Memory Bound*, you might wonder: *"Am I bound by the L1 cache, the L3 cache, or the Main Memory (DRAM)?"* - - This is where Topdown Level 3 and Level 4 come in (often requiring specific `perf stat -M` metrics depending on your architecture). - For the moment depending of your microarchitecture XTC only support up to L2 topdown, the linux perf tool will be needed to go futher. - - The **Memory Bound** category further splits into: - 1. **L1 Bound (L3):** Data is not in registers, but found extremely quickly in L1 cache. Often caused by high memory latency per instruction or bank conflicts. - 2. **L2 Bound (L3):** Data missed L1 but was found in L2. - 3. **L3 Bound (L3):** Data missed L1/L2 but was found in L3. - 4. **Ext. Memory Bound (L3):** Data missed ALL caches and had to be fetched from Main RAM. This is catastrophic for performance! - * *Splits into L4:* **Mem Bandwidth** (the memory bus is saturated) vs **Mem Latency** (waiting for the RAM chip to respond). - 5. **Store Bound (L3):** The CPU's store buffers are full because it is writing too much data to memory too quickly. - - > *Try increasing the `tile` values in the code sandbox to something huge. You will see the **Memory Bound** spike because the data chunk no longer fits in the fast caches!* - """) - return - - -@app.cell -def _(mo): - _sandbox_default = '''[ - "instructions", - "branches", - "branch-misses", - "L1-dcache-loads", - "L1-dcache-load-misses" - ]''' - sandbox_editor = mo.ui.code_editor( - value=_sandbox_default, - language="python", - ) - - sandbox_form = sandbox_editor.form(submit_button_label=" Run Custom Metrics") - return (sandbox_form,) - - -@app.cell -def _(mo, sandbox_form): +def _(btn_sandbox, sandbox_editor): mo.vstack([ mo.md(r""" --- @@ -501,54 +421,70 @@ def _(mo, sandbox_form): > CPUs only has a few programmable counters (usually 4 or 8). - > * TMA metrics (like `TopdownL1`) consume up to 5 counters at once! - > * If you ask for too many events, the CPU will run out of hardware counters. Linux will silently disable the overflowing ones. - > * **Symptom:** You will see mathematically aberrant data, such as `[-0.0, -0.0]` or an absurdly high ones.If you see this, shorten your list! + > * Overflowing events from somes TMA can be handle by a multi-passes mechanic in XTC.* + + > * **Symptom:** You will see mathematically aberrant data, such as `[-0.0, ..., -0.0]` or an absurdly high ones.If you see this, shorten your list! *This cell will not run automatically. You must click the button to evaluate the kernel.* """), - sandbox_form + sandbox_editor, + btn_sandbox ]) return @app.cell -def _(mo, module, sandbox_form): - mo.stop(sandbox_form.value is None, mo.md("*Click 'Run Custom Metrics' to see the results.*")) +def _(mo): + tma_support_content = mo.md( + """ + | Microarchitecture | Supported TMA Levels | Execution Mode | + |---|---|---| + | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2` | Native API *(Multi-pass)* | + | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2` | Native API *(Single-pass)* | + | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API *(Multi-pass)* | + | **Generic Linux (`perf` tool)** | `TopdownL1` -> `TopdownL6` | System Fallback *(Multiplexed)* | + + *Metrics beyond L2 or unmapped architectures will automatically use the `perf` fallback.* + """ + ) + + supported_arch_msg = mo.accordion({ + "💡 View supported TMA architectures": tma_support_content + }) + + return supported_arch_msg, + + +@app.cell +def _(supported_arch_msg): + supported_arch_msg + +@app.cell +def _(btn_sandbox, module, sandbox_editor): + mo.stop(not btn_sandbox.value, mo.md("*Click 'Run Custom Metrics' to see the results.*")) if module is None: sandbox_output = mo.md("**Module is not compiled.** Please fix the compilation sandbox above first.") else: import ast try: - # Safely parse the user's string into a Python list - custom_counters = ast.literal_eval(sandbox_form.value) - if not isinstance(custom_counters, list): - raise ValueError("Input must be a Python list.") + custom_counters = ast.literal_eval(sandbox_editor.value) + if not isinstance(custom_counters, list): raise ValueError("Input must be a Python list.") evaluator_sb = module.get_evaluator(validate=False, pmu_counters=custom_counters) results_sb, code_sb, err_sb = evaluator_sb.evaluate() - output_lines = [f"**Execution Code:** `{code_sb}`"] + output_lines = [f"**Execution Code:** `{code_sb}`\n"] - if err_sb: - output_lines.append(f"**Terminal Output / Errors:**\n```text\n{err_sb}\n```") output_lines.append("**Raw Results:**\n```text") - 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 - "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 - } + + DERIVED_METRICS_SIZES = {"TopdownL1": 4, "TopdownL2": 8, "TopdownL3": 26, "TopdownL4": 32, "TopdownL5": 15, "TopdownL6": 8} current_idx = 0 - hw_limit_exceeded = False for c in custom_counters: size = DERIVED_METRICS_SIZES.get(c, 1) @@ -558,35 +494,19 @@ def _(mo, module, sandbox_form): output_lines.append(f"{c:35} : {int(chunk[0])}") else: rounded_chunk = [round(x, 2) for x in chunk] - output_lines.append(f"{c:35} : {rounded_chunk} (%)") - - # Missing hardware counters detection - if rounded_chunk == [0.0, 0.0, 0.0, 100.0] or all(x == 0.0 for x in rounded_chunk): - hw_limit_exceeded = True - + output_lines.append(f"{c:35} : {rounded_chunk}") current_idx += size output_lines.append("```") - if hw_limit_exceeded: - alert_msg = ( - " **Hardware Counter Limit Exceeded!**\n\n" - "Your CPU only has a limited number of programmable counters (usually 4 or 8). " - "You requested too many events at the same time. The Linux kernel failed to schedule the TMA events, " - "returning zeros (which mathematically defaults to 100% Backend Bound).\n\n" - "**Fix:** Remove some PMU counters from your list to make room for TMA." - ) - output_lines.append(mo.callout(mo.md(alert_msg), kind="danger")._repr_html_()) + if err_sb: + output_lines.append(f"**Terminal Output / Errors:**\n```text\n{err_sb}\n```") sandbox_output = mo.md("\n".join(output_lines)) except Exception as e: sandbox_output = mo.md(f"**Error parsing your list:** {e}") - return (sandbox_output,) - -@app.cell -def _(sandbox_output): sandbox_output return From 7403874abafa0f66a28a669e5e3d598304c2bd04 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Jun 2026 08:51:03 +0200 Subject: [PATCH 27/92] [TMA]update tested arch (tma) --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 1bc717ac..f8f360f6 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -509,9 +509,9 @@ static void compute_amd_zen4_tma_l2(const double *raw, double *final) { /* * Tested TopdownL1 : - * - INTEL_SKYLAKE_CASCADE (skylake) L1,L2 + * - INTEL_SKYLAKE_CASCADE (skylake, kaby-lake)L1,L2 * - INTEL_ICELAKE_SAPPHIRE (raptor lake) L1,L2 - * - AMD_ZEN_4 L1,L2 + * - AMD_ZEN_4 (Zen 4) L1,L2 * * Untested : * - AMD_ZEN_1_2 From add94b0f59acc8895690ca67ff30f1000726fd7e Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Jun 2026 10:54:26 +0200 Subject: [PATCH 28/92] [TMA]Update HW tutorial --- docs/tutorials/hw_counters_introduction.py | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 04234748..74c3afb8 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -457,6 +457,75 @@ def _(mo): return supported_arch_msg, +@app.cell +def _(mo): + tma_output_content = mo.md( + """ + TMA hierarchically breaks down CPU bottlenecks. When the native C API evaluates `TopdownL1` or `TopdownL2`, it returns a raw array of percentages. Here is the exact index mapping: + + ### Level 1 (4 metrics) + **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. | + + ### Level 2 (8 metrics) + **Array Order:** `[Light Ops, Heavy Ops, Machine Clears, Branch Mispredicts, Fetch Bandwidth, Fetch Latency, Core Bound, Memory Bound]` + + | Index | Category | Sub-Metric | + |---|---|---| + | `[0]` | 🟢 `Retiring` | `light_operations` | + | `[1]` | 🟢 `Retiring` | `heavy_operations` | + | `[2]` | 🔴 `Bad Speculation` | `machine_clears` | + | `[3]` | 🔴 `Bad Speculation` | `branch_mispredicts` | + | `[4]` | 🔵 `Frontend Bound` | `fetch_bandwidth` | + | `[5]` | 🔵 `Frontend Bound` | `fetch_latency` | + | `[6]` | 🟣 `Backend Bound` | `core_bound` | + | `[7]` | 🟣 `Backend Bound` | `memory_bound` | + + ### Level 3 (26 metrics) + Deep dive into L2 categories. Key metrics returned in the array include: + * **Memory Subsystem:** `l1_bound`, `l2_bound`, `l3_bound`, `dram_bound`, `store_bound`, `pmm_bound` + * **Frontend Fetch:** `dsb` (decoded uop cache), `mite` (legacy decode), `icache_misses`, `itlb_misses`, `lcp` + * **Execution & ALU:** `fp_arith`, `ports_utilization`, `divider`, `serializing_operation` + * **Retiring Details:** `memory_operations`, `fused_instructions`, `microcode_sequencer` + * **Branch Prediction:** `branch_resteers`, `other_mispredicts`, `other_nukes` + + ### Level 4 (32 metrics) + Granular details on memory behavior and port utilization. + * **Memory Bandwidth & Latency:** `mem_bandwidth`, `mem_latency`, `l1_hit_latency`, `l3_hit_latency` + * **Memory Bottlenecks:** `split_loads`, `split_stores`, `4k_aliasing`, `dtlb_load`, `dtlb_store`, `false_sharing` + * **Port Utilization:** `ports_utilized_0`, `ports_utilized_1`, `ports_utilized_2`, `ports_utilized_3m` + * **Operations:** `fp_scalar`, `fp_vector`, `nop_instructions`, `x87_use`, `cisc`, `assists` + + ### Level 5 (15 metrics) + Specific unit utilization and Translation Lookaside Buffer (TLB) deep dives. + * **Operations:** `alu_op_utilization`, `load_op_utilization`, `store_op_utilization` + * **Vector Widths:** `fp_vector_128b`, `fp_vector_256b`, `fp_vector_512b` + * **STLB:** `load_stlb_hit/miss`, `store_stlb_hit/miss` + * **Memory/Cache:** `local_mem`, `remote_mem`, `remote_cache` + + ### Level 6 (8 metrics) + * **`port_0` to `port_7`:** Fraction of cycles the CPU dispatched uops on specific hardware execution ports (e.g., ALU, Loads, Stores, Branches). + """ + ) + + reminder_output_msg = mo.accordion({ + "💡 Output reminder & Topdown Metrics Dictionary": tma_output_content + }) + + return reminder_output_msg, + + +@app.cell +def _(reminder_output_msg): + reminder_output_msg + return + @app.cell def _(supported_arch_msg): From 7079c66ead3caf55c511ec4155b3a27ae464390c Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Jun 2026 10:54:58 +0200 Subject: [PATCH 29/92] [TMA]Skylake TopdownL3 (memory) implementation in progress --- .../csrcs/runtimes/host/perf_event_linux.c | 10 +++ src/xtc/csrcs/runtimes/host/perf_metrics.c | 70 +++++++++++++++++-- src/xtc/utils/evaluation.py | 5 +- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 1828f890..fdd32835 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -253,6 +253,16 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.event = 0x00c5; else if (strcmp(name, "@skl_machine_clears") == 0) event->args.config_pair.event = 0x01c3; + else if (strcmp(name, "@skl_stalls_mem_any") == 0) + event->args.config_pair.event = 0x14001414; + else if (strcmp(name, "@skl_stalls_l1d_miss") == 0) + event->args.config_pair.event = 0x0c000c14; + else if (strcmp(name, "@skl_stalls_l2_miss") == 0) + event->args.config_pair.event = 0x05000514; + else if (strcmp(name, "@skl_stalls_l3_miss") == 0) + event->args.config_pair.event = 0x06000614; + else if (strcmp(name, "@skl_bound_on_stores") == 0) + event->args.config_pair.event = 0x08a2; else return 1; diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index f8f360f6..22a90215 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -237,19 +237,19 @@ static void compute_skl_tma_l1(const double *raw_values, double *final_results) static const int skl_l2_passes[] = {4, 4, 4}; // 3 passes of 4 counters static const char *skl_tma_l2_events[] = { - // Passe 1 : L1 Base + Backend + // Pass 1 : L1 Base + Backend "@skl_slots", "cycle_activity:stalls_mem_any", // Memory Bound "resource_stalls:any", // Core Bound base "@skl_fe_bound", - // Passe 2 : Frontend + // Pass 2 : Frontend "@skl_slots", "icache_16b:ifdata_stall", // Fetch Latency "icache_64b:iftag_stall", // Fetch Latency "@skl_retiring", - // Passe 3 : Retiring + Bad Speculation + // Pass 3 : Retiring + Bad Speculation "@skl_slots", "idq:ms_uops", // Heavy Ops "br_misp_retired:all_branches", // Branch Mispredicts @@ -258,9 +258,9 @@ static const char *skl_tma_l2_events[] = { static void compute_skl_tma_l2(const double *raw, double *final) { - // Passe 1 : 0 (slots), 1 (mem), 2 (core), 3 (fe) - // Passe 2 : 4 (slots), 5 (lat_d), 6 (lat_t), 7 (ret) - // Passe 3 : 8 (slots), 9 (heavy), 10 (misp), 11 (clears) + // 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; @@ -312,6 +312,47 @@ static void compute_skl_tma_l2(const double *raw, double *final) { } } +static const int skl_l3_mem_passes[] = {4, 1}; + +static const char *skl_tma_l3_mem_events[] = { + // Pass 1 + "@skl_slots", // + "@skl_stalls_mem_any", // 0x14001414 (cmask=0x14, umask=0x14, event=0x14) + "@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 slots_p1 = raw[0] * 4.0; + double slots_p2 = raw[4] * 4.0; + + if (slots_p1 > 0 && slots_p2 > 0) { + double mem_any = raw[1] / slots_p1; + double l1d_miss = raw[2] / slots_p1; + double l2_miss = raw[3] / slots_p1; + + double l3_miss = raw[5] / slots_p2; + double stores = raw[6] / slots_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; + } +} + + // === Modern Inter arch === static const char *intel_modern_tma_l1_events[] = { @@ -632,6 +673,23 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { } return 0; } + else if (strcmp(metric_name, "TopdownL3_Mem") == 0) { + if (detect_if_intel()) { + intel_arch_t uarch = detect_intel_microarchitecture(); + if (uarch == INTEL_SKYLAKE_CASCADE) { + fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L3_Mem (Multi-Pass)\n"); + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 7; + out_resolver->hw_events = skl_tma_l3_mem_events; + out_resolver->num_results = 5; + out_resolver->num_passes = 2; + out_resolver->events_per_pass = skl_l3_mem_passes; + out_resolver->compute_formula = compute_skl_tma_l3_mem; + return 1; + } + } + return 0; + } // Unsuported hardware / metric or the event is a pmu return 0; diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index 52c40304..c552d550 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -159,6 +159,7 @@ def validate_outputs( "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 @@ -241,7 +242,9 @@ def evaluate_performance( # Fallback on linux perf tool if failed_counters: - print(f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'...") + print( + f"[WARNING] Some hardware counters failed: {failed_counters}. Fallback to 'perf stat'..." + ) perf_path = shutil.which("perf") if not perf_path: From 93d2a14f152ba5c60e02589893f5d3b251d6bec2 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 9 Jun 2026 09:32:31 +0200 Subject: [PATCH 30/92] [TMA]usage for TopdownL3_Mem --- docs/tutorials/hw_counters_introduction.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 74c3afb8..d11791a0 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -442,11 +442,14 @@ def _(mo): """ | Microarchitecture | Supported TMA Levels | Execution Mode | |---|---|---| - | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2` | Native API *(Multi-pass)* | + | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem` | Native API *(Multi-pass)* | | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2` | Native API *(Single-pass)* | | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API *(Multi-pass)* | | **Generic Linux (`perf` tool)** | `TopdownL1` -> `TopdownL6` | System Fallback *(Multiplexed)* | + + *Note : TopdownL3_Mem return [L1 Bound, L2 Bound, L3 Bound, DRAM Bound, Store Bound]* + *Metrics beyond L2 or unmapped architectures will automatically use the `perf` fallback.* """ ) From 8e9162c7647a49526288d3f2b1c49747bc563dfe Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 9 Jun 2026 09:33:20 +0200 Subject: [PATCH 31/92] [TMA]fix TopdownL3_Mem API for skylake --- .../csrcs/runtimes/host/perf_event_linux.c | 8 ++++---- src/xtc/csrcs/runtimes/host/perf_metrics.c | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index fdd32835..f9fed97a 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -254,13 +254,13 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) else if (strcmp(name, "@skl_machine_clears") == 0) event->args.config_pair.event = 0x01c3; else if (strcmp(name, "@skl_stalls_mem_any") == 0) - event->args.config_pair.event = 0x14001414; + event->args.config_pair.event = 0x140014a3; else if (strcmp(name, "@skl_stalls_l1d_miss") == 0) - event->args.config_pair.event = 0x0c000c14; + event->args.config_pair.event = 0x0c000ca3; else if (strcmp(name, "@skl_stalls_l2_miss") == 0) - event->args.config_pair.event = 0x05000514; + event->args.config_pair.event = 0x050005a3; else if (strcmp(name, "@skl_stalls_l3_miss") == 0) - event->args.config_pair.event = 0x06000614; + event->args.config_pair.event = 0x060006a3; else if (strcmp(name, "@skl_bound_on_stores") == 0) event->args.config_pair.event = 0x08a2; else diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 22a90215..b9792a63 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -312,7 +312,7 @@ static void compute_skl_tma_l2(const double *raw, double *final) { } } -static const int skl_l3_mem_passes[] = {4, 1}; +static const int skl_l3_mem_passes[] = {4, 3}; static const char *skl_tma_l3_mem_events[] = { // Pass 1 @@ -329,16 +329,17 @@ static const char *skl_tma_l3_mem_events[] = { // Compute only memory bound from L3 static void compute_skl_tma_l3_mem(const double *raw, double *final) { - double slots_p1 = raw[0] * 4.0; - double slots_p2 = raw[4] * 4.0; + double cycles_p1 = raw[0]; + double cycles_p2 = raw[4]; - if (slots_p1 > 0 && slots_p2 > 0) { - double mem_any = raw[1] / slots_p1; - double l1d_miss = raw[2] / slots_p1; - double l2_miss = raw[3] / slots_p1; + if (cycles_p1 > 0 && cycles_p2 > 0) { + // On divise directement par les cycles ! + 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] / slots_p2; - double stores = raw[6] / slots_p2; + 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 From 9c227e301db4232d6cfa2a4c777ac67b63b59089 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 9 Jun 2026 16:38:39 +0200 Subject: [PATCH 32/92] [TMA]altair dependency to show graphes in marimo --- requirements_dev.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index a33b7078..0b888e5b 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 + From e2ca7be3e8839fd6114e9c00df3c4691829e8745 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 9 Jun 2026 16:39:43 +0200 Subject: [PATCH 33/92] [TMA]fix events code --- src/xtc/csrcs/runtimes/host/perf_event_linux.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index f9fed97a..ed8a8461 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -238,7 +238,7 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) else if (strcmp(name, "@skl_retiring") == 0) event->args.config_pair.event = 0x02c2; else if (strcmp(name, "@skl_recovery") == 0) - event->args.config_pair.event = 0x0100019d; + event->args.config_pair.event = 0x0120010d; else if (strcmp(name, "@skl_mem_stalls") == 0) event->args.config_pair.event = 0x1414; else if (strcmp(name, "@skl_core_stalls") == 0) @@ -246,13 +246,13 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) else if (strcmp(name, "@skl_fetch_lat_data") == 0) event->args.config_pair.event = 0x0480; else if (strcmp(name, "@skl_fetch_lat_tag") == 0) - event->args.config_pair.event = 0x0183; + event->args.config_pair.event = 0x0483; else if (strcmp(name, "@skl_heavy_ops") == 0) event->args.config_pair.event = 0x3079; else if (strcmp(name, "@skl_br_misp") == 0) event->args.config_pair.event = 0x00c5; else if (strcmp(name, "@skl_machine_clears") == 0) - event->args.config_pair.event = 0x01c3; + event->args.config_pair.event = 0x010401c3; else if (strcmp(name, "@skl_stalls_mem_any") == 0) event->args.config_pair.event = 0x140014a3; else if (strcmp(name, "@skl_stalls_l1d_miss") == 0) From 1434e3bb425d4ef45b167f63d04778115de53b8b Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 9 Jun 2026 16:50:33 +0200 Subject: [PATCH 34/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 126 +++++++++++++++------ 1 file changed, 89 insertions(+), 37 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index d11791a0..4ac6f0b2 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -15,7 +15,7 @@ @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" # Hardware Performance Counters & Top-down Analysis @@ -29,7 +29,7 @@ def _(): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" > **Disclaimer and prerequisites:** > Results will vary depending on your hardware architecture (MacOS is currently not supported). @@ -45,7 +45,7 @@ def _(): @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## 1. Defining the Workload & Schedule We define a medium-sized matrix multiplication (1024x2048x4096). @@ -58,7 +58,7 @@ def _(): @app.cell -def _(): +def _(mo): # UI sliders tile_i_ui = mo.ui.slider(start=1, stop=64, step=1, value=3, label="Tile I (Rows)") tile_j_ui = mo.ui.slider(start=8, stop=512, step=8, value=24, label="Tile J (Cols)") @@ -67,7 +67,7 @@ def _(): @app.cell -def _(): +def _(mo): _editor_code = '''import xtc.graphs.xtc.op as O from xtc.backends.mlir import Backend from xtc.schedules.descript import descript_scheduler @@ -148,7 +148,7 @@ def _(descript_editor, tile_i_ui, tile_j_ui, unroll_ui): @app.cell -def _(I_val, J_val, K_val, tile_i_ui, tile_j_ui, unroll_ui): +def _(I_val, J_val, K_val, mo, os, sys, tile_i_ui, tile_j_ui, unroll_ui): # Fetch hardware cache sizes dynamically (sysfs) caches_kb = {} if sys.platform == "linux" and os.path.exists("/sys/devices/system/cpu/cpu0/cache"): @@ -239,7 +239,7 @@ def _(sidebar): @app.cell -def _(): +def _(mo): # On-demand execution buttons btn_pmu = mo.ui.run_button(kind="info", label="Evaluate Raw PMUs") btn_l1 = mo.ui.run_button(kind="info", label="Evaluate Topdown L1") @@ -252,7 +252,7 @@ def _(): @app.cell(hide_code=True) -def _(btn_pmu): +def _(btn_pmu, mo): mo.md(f""" ## 2. Raw Hardware Counters (PMU) CPUs expose raw counters to track specific events. We can ask the CPU exactly how many cycles were spent or how many L1/L2 cache misses occurred. @@ -261,20 +261,16 @@ def _(btn_pmu): {btn_pmu} """) - return @app.cell -def _(btn_pmu, exec_error, module): - # Wait for button click +def _(btn_pmu, exec_error, mo, module): mo.stop(not btn_pmu.value, mo.md("*Click the button above to execute PMU Evaluation.*")) if module is None: - pmu_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") + pmu_ui = mo.md(f"**Compilation Error:**\n```python\n{exec_error}\n```") else: - pmu_counters = ["cycles", "instructions"] - if platform == "linux": - pmu_counters += ["mem_load_retired.l1_miss", "mem_load_retired.l2_miss"] + pmu_counters = ["cycles", "instructions", "cache_access", "cache_misses", "branches", "branches_misses"] evaluator_pmu = module.get_evaluator(validate=True, pmu_counters=pmu_counters) results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() @@ -285,13 +281,12 @@ def _(btn_pmu, exec_error, module): mo.ui.table(_pmu_data, label="Raw PMU Results") ]) - # Render UI pmu_ui return @app.cell(hide_code=True) -def _(btn_l1): +def _(btn_l1, mo): mo.md(f""" ## 3. Top-down Microarchitecture Analysis (Level 1) Raw counters are hard to interpret. To solve this, **TMA** (Top-down Analysis) groups all CPU pipeline slots into 4 distinct categories, summing up to 100%: @@ -301,12 +296,13 @@ def _(btn_l1): * 🔴 **Bad Speculation:** The CPU guessed a branch wrong and had to flush its pipeline. * 🔵 **Frontend Bound:** The CPU is starved; it cannot fetch/decode instructions fast enough. * 🟣 **Backend Bound:** The CPU is waiting (usually for Memory or Execution Units) to finish the current instructions. + + {btn_l1} """) - return @app.cell -def _(btn_l1, exec_error, module): +def _(btn_l1, exec_error, mo, module, platform): mo.stop(not btn_l1.value, mo.md("*Click the button above to execute Topdown L1 Evaluation.*")) if module is None: @@ -320,24 +316,49 @@ def _(btn_l1, exec_error, module): if tma_l1_counters and len(results_l1) > 0: if results_l1[0] < 0: - _l1_ui_table = mo.accordion({"Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```")}) + _l1_ui_display = mo.accordion({"Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```")}) else: _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") + + try: + import altair as alt + + _chart = alt.Chart(alt.Data(values=_l1_data)).mark_arc(innerRadius=50).encode( + theta=alt.Theta(field="Percentage (%)", type="quantitative"), + color=alt.Color( + field="Category", + type="nominal", + scale=alt.Scale( + domain=_l1_labels, + range=["#4ade80", "#ff4a4a", "#60a5fa", "#c084fc"] # Vert, Rouge, Bleu, Violet + ), + legend=alt.Legend(title="Bottlenecks") + ), + tooltip=["Category:N", "Percentage (%):Q"] + ).properties(width=300, height=300, title="TMA L1 Breakdown") + + _l1_ui_display = mo.hstack([_l1_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) + + except ImportError: + _l1_ui_display = mo.vstack([ + mo.md("*Tip: Install `altair` (`pip install altair`) to see the interactive pie chart!*"), + _l1_ui_table + ]) else: - _l1_ui_table = mo.md("*TMA L1 is not supported on this machine.*") + _l1_ui_display = mo.md("*TMA L1 is not supported on this machine.*") l1_ui = mo.vstack([ mo.md(f"**Execution Code:** `{code_l1}`"), - _l1_ui_table + _l1_ui_display ]) l1_ui - return + return evaluator_l1, l1_ui, results_l1, tma_l1_counters -@app.cell(hide_code=True) -def _(btn_l2): +@app.cell +def _(btn_l2, mo): mo.md(f""" ## 4. Drilling Down (Topdown Level 2) If our code is heavily **Backend Bound**, we need to know why. TMA Level 2 splits the L1 categories further: @@ -351,7 +372,7 @@ def _(btn_l2): @app.cell -def _(btn_l2, exec_error, module): +def _(btn_l2, exec_error, mo, module, platform): mo.stop(not btn_l2.value, mo.md("*Click the button above to execute Topdown L2 Evaluation.*")) if module is None: @@ -374,24 +395,54 @@ def _(btn_l2, exec_error, module): if tma_l2_counters and len(results_l2) > 0: if results_l2[0] < 0: - _l2_ui_table = mo.vstack([ - mo.callout(mo.md("*Internal C resolver unsupported for L2. Showing perf stat output below.*"), kind="warn"), + _l2_ui_display = mo.vstack([ + mo.callout(mo.md("*Internal C resolver unsupported for L2. Showing `perf stat` output below.*"), kind="warn"), mo.accordion({"Terminal Output from perf": mo.md(f"```text\n{error_l2}\n```")}) ]) else: _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] _l2_ui_table = mo.ui.table(_l2_data, label="Topdown L2 Results") + + try: + import altair as alt2 + + _chart = alt2.Chart(alt2.Data(values=_l2_data)).mark_arc(innerRadius=50).encode( + theta=alt2.Theta(field="Percentage (%)", type="quantitative"), + color=alt2.Color( + field="Sub-Category", + type="nominal", + scale=alt2.Scale( + domain=_l2_labels, + range=[ + "#4ade80", "#22c55e", # light green, deep green (Retiring) + "#f87171", "#ef4444", # light red, deep red (Bad Spec) + "#60a5fa", "#3b82f6", # light blue, deep blue (Frontend) + "#c084fc", "#a855f7" # light purple, deep purple (Backend) + ] + ), + legend=alt2.Legend(title="Sub-Bottlenecks") + ), + tooltip=["Sub-Category:N", "Percentage (%):Q"] + ).properties(width=350, height=300, title="TMA L2 Breakdown") + + _l2_ui_display = mo.hstack([_l2_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) + + except ImportError: + _l2_ui_display = mo.vstack([ + mo.md("*Tip: Install `altair` (`pip install altair`) to see the interactive pie chart!*"), + _l2_ui_table + ]) else: - _l2_ui_table = mo.md("*TMA L2 is not supported on this machine.*") + _l2_ui_display = mo.md("*TMA L2 is not supported on this machine.*") - l2_ui = mo.vstack([mo.md(f"**Execution Code:** `{code_l2}`"), _l2_ui_table]) + l2_ui = mo.vstack([mo.md(f"**Execution Code:** `{code_l2}`"), _l2_ui_display]) l2_ui - return + return evaluator_l2, l2_ui, results_l2, tma_l2_counters @app.cell(hide_code=True) -def _(): +def _(mo): mo.md(r""" ## How to fix Core Bound vs Memory Bound? @@ -409,7 +460,7 @@ def _(): @app.cell(hide_code=True) -def _(btn_sandbox, sandbox_editor): +def _(btn_sandbox, mo, sandbox_editor): mo.vstack([ mo.md(r""" --- @@ -457,8 +508,8 @@ def _(mo): supported_arch_msg = mo.accordion({ "💡 View supported TMA architectures": tma_support_content }) + return (supported_arch_msg,) - return supported_arch_msg, @app.cell def _(mo): @@ -520,8 +571,7 @@ def _(mo): reminder_output_msg = mo.accordion({ "💡 Output reminder & Topdown Metrics Dictionary": tma_output_content }) - - return reminder_output_msg, + return (reminder_output_msg,) @app.cell @@ -533,9 +583,11 @@ def _(reminder_output_msg): @app.cell def _(supported_arch_msg): supported_arch_msg + return + @app.cell -def _(btn_sandbox, module, sandbox_editor): +def _(btn_sandbox, mo, module, sandbox_editor): mo.stop(not btn_sandbox.value, mo.md("*Click 'Run Custom Metrics' to see the results.*")) if module is None: From bb0827c9de144dca52c59ca54df22399e409f668 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 09:55:39 +0200 Subject: [PATCH 35/92] [TMA]add L3_Mem metric --- .../evaluation/test_matmul_tma_counters.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index 4cd392a1..1ae7fc3c 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -38,7 +38,7 @@ # Linux Perf counters if sys.platform == "linux": hw_counters += [ - "TopdownL1","TopdownL2" + "TopdownL1","TopdownL2", "TopdownL3_Mem" ] elif sys.platform == "darwin": # On MacOS, requires sudo to get counters @@ -93,6 +93,17 @@ def get_c(val): print(f"{'L2 Core bound':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") print(f"{'L2 Memory bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") +print("") +# L3 Memory Metrics +print(f"{'L3 L1 bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") +print(f"{'L3 L2 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") +print(f"{'L3 L3 bound':<{w}}: {get_c(results[13])}{results[13]:.2f}{RESET}") +print(f"{'L3 DRAM bound':<{w}}: {get_c(results[14])}{results[14]:.2f}{RESET}") +print(f"{'L3 store bound':<{w}}: {get_c(results[15])}{results[15]:.2f}{RESET}") + + + + # CHECK: CODE: 0 # CHECK-NEXT: counters: # CHECK-NEXT: results: From 4b379ef2249d426cf6bb2af1d9e5a6863358ef08 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 09:56:20 +0200 Subject: [PATCH 36/92] [TMA]topdownL3 (memory) support for modern Intel --- .../csrcs/runtimes/host/perf_event_linux.c | 17 ++++++++++- src/xtc/csrcs/runtimes/host/perf_metrics.c | 29 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index ed8a8461..de703632 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -228,7 +228,7 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) if (strncmp(name, "@skl_", 5) == 0) { event->mode = PERF_ARG_GENERIC; event->args.config_pair.type = PERF_TYPE_RAW; - + // TopdownL1 if (strcmp(name, "@skl_slots") == 0) event->args.config_pair.event = 0x003c; else if (strcmp(name, "@skl_fe_bound") == 0) @@ -237,6 +237,7 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.event = 0x010e; else if (strcmp(name, "@skl_retiring") == 0) event->args.config_pair.event = 0x02c2; + // TopdownL2 else if (strcmp(name, "@skl_recovery") == 0) event->args.config_pair.event = 0x0120010d; else if (strcmp(name, "@skl_mem_stalls") == 0) @@ -253,6 +254,7 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.event = 0x00c5; else if (strcmp(name, "@skl_machine_clears") == 0) event->args.config_pair.event = 0x010401c3; + // TopdownL3_Mem else if (strcmp(name, "@skl_stalls_mem_any") == 0) event->args.config_pair.event = 0x140014a3; else if (strcmp(name, "@skl_stalls_l1d_miss") == 0) @@ -333,6 +335,19 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) event->args.config_pair.event = 0x8600; else if (strcmp(name, "@icl_mem_bound") == 0) event->args.config_pair.event = 0x8700; + // TopdownL3_Mem + else if (strcmp(name, "@icl_cyc") == 0) + event->args.config_pair.event = 0x003c; + else if (strcmp(name, "@icl_stalls_mem_any") == 0) + event->args.config_pair.event = 0x140014a3; + else if (strcmp(name, "@icl_stalls_l1d_miss") == 0) + event->args.config_pair.event = 0x0c000ca3; + else if (strcmp(name, "@icl_stalls_l2_miss") == 0) + event->args.config_pair.event = 0x050005a3; + else if (strcmp(name, "@icl_stalls_l3_miss") == 0) + event->args.config_pair.event = 0x060006a3; + else if (strcmp(name, "@icl_bound_on_stores") == 0) + event->args.config_pair.event = 0x40a6; else return 1; diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index b9792a63..dffe2516 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -201,7 +201,6 @@ static const int pass_9[] = {9}; // === Skylake arch === - static const char *skl_tma_l1_events[] = { "@skl_slots", // 0x003c (Cycles) "@skl_fe_bound", // 0x019c @@ -317,7 +316,7 @@ 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", // 0x14001414 (cmask=0x14, umask=0x14, event=0x14) + "@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) @@ -333,7 +332,6 @@ static void compute_skl_tma_l3_mem(const double *raw, double *final) { double cycles_p2 = raw[4]; if (cycles_p1 > 0 && cycles_p2 > 0) { - // On divise directement par les cycles ! double mem_any = raw[1] / cycles_p1; double l1d_miss = raw[2] / cycles_p1; double l2_miss = raw[3] / cycles_p1; @@ -431,6 +429,21 @@ static void compute_intel_modern_tma_l2(const double *raw, double *final) { } } +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" +}; + // === Zen arch === static const char *amd_zen4_tma_l1_events[] = { @@ -688,6 +701,16 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->compute_formula = compute_skl_tma_l3_mem; return 1; } + else if (uarch == INTEL_ICELAKE_SAPPHIRE) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 7; + out_resolver->hw_events = intel_modern_tma_l3_mem_events; + out_resolver->num_results = 5; + out_resolver->num_passes = 2; + out_resolver->events_per_pass = icl_l3_mem_passes; + out_resolver->compute_formula = compute_skl_tma_l3_mem; // same as skl + return 1; + } } return 0; } From 05f116e0bc00c3aebd42d0985d3fecb0a94e066d Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 09:56:42 +0200 Subject: [PATCH 37/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 4ac6f0b2..d90b0cad 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -9,9 +9,9 @@ import marimo as mo from sys import platform - notebook_dir = os.path.dirname(os.path.realpath(__file__)) - project_root = os.path.abspath(os.path.join(notebook_dir, "..", "..")) - os.chdir(project_root) +# notebook_dir = os.path.dirname(os.path.realpath(__file__)) +# project_root = os.path.abspath(os.path.join(notebook_dir, "..", "..")) +# os.chdir(project_root) @app.cell(hide_code=True) @@ -62,7 +62,7 @@ def _(mo): # UI sliders tile_i_ui = mo.ui.slider(start=1, stop=64, step=1, value=3, label="Tile I (Rows)") tile_j_ui = mo.ui.slider(start=8, stop=512, step=8, value=24, label="Tile J (Cols)") - unroll_ui = mo.ui.slider(start=1, stop=8, step=1, value=2, label="Unroll factor") + unroll_ui = mo.ui.slider(start=1, stop=32, step=1, value=2, label="Unroll factor") return tile_i_ui, tile_j_ui, unroll_ui From 1b7694b7b5cb0926af7ec59e0d2630ae3f61b871 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 10:36:55 +0200 Subject: [PATCH 38/92] [TMA]improve perf by using better schedule --- docs/tutorials/hw_counters_introduction.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index d90b0cad..844aa6ef 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -60,8 +60,8 @@ def _(mo): @app.cell def _(mo): # UI sliders - tile_i_ui = mo.ui.slider(start=1, stop=64, step=1, value=3, label="Tile I (Rows)") - tile_j_ui = mo.ui.slider(start=8, stop=512, step=8, value=24, label="Tile J (Cols)") + tile_i_ui = mo.ui.slider(start=8, stop=1024, step=8, value=16, label="Tile I (Rows)") + tile_j_ui = mo.ui.slider(start=8, stop=1024, step=8, value=16, label="Tile J (Cols)") unroll_ui = mo.ui.slider(start=1, stop=32, step=1, value=2, label="Unroll factor") return tile_i_ui, tile_j_ui, unroll_ui @@ -85,11 +85,11 @@ def _(mo): # Schedule specification schedule_spec = { - "j": {}, "i": {}, "k": {}, - f"j#{slider_j}": {"unroll": slider_unroll}, - f"i#{slider_i}": {"vectorize": True} + "j": {}, + f"i#{slider_i}": {"unroll": slider_unroll}, + f"j#{slider_j}": {"vectorize": True} } # Compile @@ -97,7 +97,7 @@ def _(mo): descript_scheduler( scheduler=scheduler, node_name="C", - abstract_dims=["j", "i", "k"], + abstract_dims=["i", "j", "k"], spec=schedule_spec ) sched = scheduler.schedule() From 31bd9048127ff5605a27f63ec1b7471b91114e9a Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 10:37:28 +0200 Subject: [PATCH 39/92] [TMA]use descript schedule --- .../evaluation/test_matmul_tma_counters.py | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index 1ae7fc3c..d65ea246 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -3,6 +3,8 @@ 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 @@ -19,19 +21,32 @@ impl = Backend(graph) -sch = impl.get_scheduler() -sch.tile("i", {"i1": 2}) -sch.tile("j", {"j1": 16}) -sch.interchange(["k", "i", "j", "i1", "j1"]) -sch.vectorize(["j1"]) -sch.unroll({"i1": 2}) -sched = sch.schedule() +# 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() -comp = impl.get_compiler( - shared_lib=True, +compiler = impl.get_compiler( dump_file="matmul_mlir", + shared_lib=True, + print_source_ir=False, + print_transformed_ir=False ) -module = comp.compile(sched) +module = compiler.compile(sched) hw_counters = [] From 28d0534af42a0af938a454e67421f94972d0608e Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 13:07:47 +0200 Subject: [PATCH 40/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 844aa6ef..b40dc54f 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -493,9 +493,9 @@ def _(mo): """ | Microarchitecture | Supported TMA Levels | Execution Mode | |---|---|---| - | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem` | Native API *(Multi-pass)* | - | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2` | Native API *(Single-pass)* | - | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API *(Multi-pass)* | + | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem` | Native API | + | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem` | Native API | + | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API | | **Generic Linux (`perf` tool)** | `TopdownL1` -> `TopdownL6` | System Fallback *(Multiplexed)* | From 8e747b307f240c51d133ed7b1a3ccb4151ee5592 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 13:28:40 +0200 Subject: [PATCH 41/92] [TMA]event fixing to test --- src/xtc/csrcs/runtimes/host/perf_event_linux.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index de703632..93a5cebf 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -271,6 +271,7 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) return 0; } + // Bits 0-7 = Event[7:0], Bits 8-15 = Umask, Bits 32-35 = Event[11:8] else if (strncmp(name, "@zen4_", 6) == 0) { event->mode = PERF_ARG_GENERIC; event->args.config_pair.type = PERF_TYPE_RAW; @@ -287,23 +288,23 @@ static int inline set_config_by_arch(const char *name, perf_event_args_t *event) // L2 else if (strcmp(name, "@zen4_be_mem") == 0) - event->args.config_pair.event = 0x1000002A9ULL; + event->args.config_pair.event = 0x1000002A9ULL; // 0x1000002A0ULL ? else if (strcmp(name, "@zen4_be_cpu") == 0) - event->args.config_pair.event = 0x1000004A9ULL; + event->args.config_pair.event = 0x1000004A9ULL; // 0x1000004A0ULL ? // L2 Frontend (cmask=0x6 add 0x06000000) else if (strcmp(name, "@zen4_fe_lat") == 0) - event->args.config_pair.event = 0x1060001A9ULL; + event->args.config_pair.event = 0x1060001A9ULL; // 0x1060001A0ULL ? else if (strcmp(name, "@zen4_fe_tot") == 0) - event->args.config_pair.event = 0x1000001A9ULL; + event->args.config_pair.event = 0x1000001A9ULL; // 0x1000001A0ULL ? // L2 Bad Speculation + Retiring else if (strcmp(name, "@zen4_bs_misp") == 0) - event->args.config_pair.event = 0x0008000C1ULL; + event->args.config_pair.event = 0x0008000C1ULL; // 0x00C3; // ex_ret_brn_misp else if (strcmp(name, "@zen4_bs_resync") == 0) - event->args.config_pair.event = 0x0002000C1ULL; + event->args.config_pair.event = 0x0002000C1ULL; // 0x0091; // bp_de_redirect else if (strcmp(name, "@zen4_ret_micro") == 0) - event->args.config_pair.event = 0x0001000C1ULL; + event->args.config_pair.event = 0x0001000C1ULL; // 0x1000000C2ULL; // ex_ret_ucode_ops else return 1; From 125573e8d402b0b101f7563a8b05c4dbe892bf1e Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 15:37:26 +0200 Subject: [PATCH 42/92] [TMA]fix missing TopdownL2 data --- .../evaluation/test_matmul_tma_counters.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index d65ea246..02f7344b 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -44,7 +44,8 @@ dump_file="matmul_mlir", shared_lib=True, print_source_ir=False, - print_transformed_ir=False + print_transformed_ir=False, + print_assembly=False ) module = compiler.compile(sched) @@ -85,6 +86,7 @@ def get_c(val): if val > 75: return ORANGE if val > 50: return YELLOW if val > 10: return GREEN + # < 10 white if val == -1: return MAGENTA return RESET @@ -104,17 +106,18 @@ def get_c(val): print(f"{'L2 Heavy ops':<{w}}: {get_c(results[5])}{results[5]:.2f}{RESET}") print(f"{'L2 Machine clear':<{w}}: {get_c(results[6])}{results[6]:.2f}{RESET}") print(f"{'L2 Branch misspredict':<{w}}: {get_c(results[7])}{results[7]:.2f}{RESET}") -print(f"{'L2 Fetch latency':<{w}}: {get_c(results[8])}{results[8]:.2f}{RESET}") -print(f"{'L2 Core bound':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") -print(f"{'L2 Memory bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") +print(f"{'L2 Fetch bandwidth':<{w}}: {get_c(results[8])}{results[8]:.2f}{RESET}") +print(f"{'L2 Fetch latency':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") +print(f"{'L2 Core bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") +print(f"{'L2 Memory bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") print("") # L3 Memory Metrics -print(f"{'L3 L1 bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") -print(f"{'L3 L2 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") -print(f"{'L3 L3 bound':<{w}}: {get_c(results[13])}{results[13]:.2f}{RESET}") -print(f"{'L3 DRAM bound':<{w}}: {get_c(results[14])}{results[14]:.2f}{RESET}") -print(f"{'L3 store bound':<{w}}: {get_c(results[15])}{results[15]:.2f}{RESET}") +print(f"{'L3 L1 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") +print(f"{'L3 L2 bound':<{w}}: {get_c(results[13])}{results[13]:.2f}{RESET}") +print(f"{'L3 L3 bound':<{w}}: {get_c(results[14])}{results[14]:.2f}{RESET}") +print(f"{'L3 DRAM bound':<{w}}: {get_c(results[15])}{results[15]:.2f}{RESET}") +print(f"{'L3 store bound':<{w}}: {get_c(results[16])}{results[16]:.2f}{RESET}") From db3acc55932e21d26179378649f56f828b9478b6 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 10 Jun 2026 15:39:03 +0200 Subject: [PATCH 43/92] [TMA]opti : not using strcmp for hw events, explicit masking for better maintainability --- .../csrcs/runtimes/host/perf_event_linux.c | 218 ++++++++---------- 1 file changed, 90 insertions(+), 128 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 93a5cebf..0b549346 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -212,6 +212,75 @@ 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) } +}; + +// INTEL MODERN (Ice Lake, Sapphire Rapids, Alder/Raptor Lake) +static const pmu_event_def_t icl_raw_events[] = { + { "@icl_slots", X86_RAW(0x00, 0x04, 0) }, + { "@icl_retiring", X86_RAW(0x00, 0x80, 0) }, + { "@icl_bad_spec", X86_RAW(0x00, 0x81, 0) }, + { "@icl_fe_bound", X86_RAW(0x00, 0x82, 0) }, + { "@icl_be_bound", X86_RAW(0x00, 0x83, 0) }, + { "@icl_heavy_ops", X86_RAW(0x00, 0x84, 0) }, + { "@icl_br_mispredict", X86_RAW(0x00, 0x85, 0) }, + { "@icl_fetch_lat", X86_RAW(0x00, 0x86, 0) }, + { "@icl_mem_bound", X86_RAW(0x00, 0x87, 0) }, + // Fallback L3 Memory events (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) } +}; + +// 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) +}; + /* * Source * Intel : https://github.com/torvalds/linux/blob/master/arch/x86/events/intel/core.c @@ -222,140 +291,33 @@ void stop_perf_events(int n_events, const int *fds, uint64_t *results) { * * todo ARM : https://developer.arm.com/documentation/ddi0434/a/performance-monitoring-unit/performance-monitoring-register-descriptions/event-type-select-register?lang=en */ -static int inline set_config_by_arch(const char *name, perf_event_args_t *event){ - - // Old Intel - if (strncmp(name, "@skl_", 5) == 0) { - event->mode = PERF_ARG_GENERIC; - event->args.config_pair.type = PERF_TYPE_RAW; - // TopdownL1 - if (strcmp(name, "@skl_slots") == 0) - event->args.config_pair.event = 0x003c; - else if (strcmp(name, "@skl_fe_bound") == 0) - event->args.config_pair.event = 0x019c; - else if (strcmp(name, "@skl_issued") == 0) - event->args.config_pair.event = 0x010e; - else if (strcmp(name, "@skl_retiring") == 0) - event->args.config_pair.event = 0x02c2; - // TopdownL2 - else if (strcmp(name, "@skl_recovery") == 0) - event->args.config_pair.event = 0x0120010d; - else if (strcmp(name, "@skl_mem_stalls") == 0) - event->args.config_pair.event = 0x1414; - else if (strcmp(name, "@skl_core_stalls") == 0) - event->args.config_pair.event = 0x01a2; - else if (strcmp(name, "@skl_fetch_lat_data") == 0) - event->args.config_pair.event = 0x0480; - else if (strcmp(name, "@skl_fetch_lat_tag") == 0) - event->args.config_pair.event = 0x0483; - else if (strcmp(name, "@skl_heavy_ops") == 0) - event->args.config_pair.event = 0x3079; - else if (strcmp(name, "@skl_br_misp") == 0) - event->args.config_pair.event = 0x00c5; - else if (strcmp(name, "@skl_machine_clears") == 0) - event->args.config_pair.event = 0x010401c3; - // TopdownL3_Mem - else if (strcmp(name, "@skl_stalls_mem_any") == 0) - event->args.config_pair.event = 0x140014a3; - else if (strcmp(name, "@skl_stalls_l1d_miss") == 0) - event->args.config_pair.event = 0x0c000ca3; - else if (strcmp(name, "@skl_stalls_l2_miss") == 0) - event->args.config_pair.event = 0x050005a3; - else if (strcmp(name, "@skl_stalls_l3_miss") == 0) - event->args.config_pair.event = 0x060006a3; - else if (strcmp(name, "@skl_bound_on_stores") == 0) - event->args.config_pair.event = 0x08a2; - else - return 1; - - return 0; +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; +} - // Bits 0-7 = Event[7:0], Bits 8-15 = Umask, Bits 32-35 = Event[11:8] - else if (strncmp(name, "@zen4_", 6) == 0) { - event->mode = PERF_ARG_GENERIC; - event->args.config_pair.type = PERF_TYPE_RAW; - - // L1 - if (strcmp(name, "@zen4_cyc") == 0) - event->args.config_pair.event = 0x0076; - else if (strcmp(name, "@zen4_fe") == 0) - event->args.config_pair.event = 0x1000001A0ULL; - else if (strcmp(name, "@zen4_disp") == 0) - event->args.config_pair.event = 0x07AA; - else if (strcmp(name, "@zen4_ret") == 0) - event->args.config_pair.event = 0x00C1; - - // L2 - else if (strcmp(name, "@zen4_be_mem") == 0) - event->args.config_pair.event = 0x1000002A9ULL; // 0x1000002A0ULL ? - else if (strcmp(name, "@zen4_be_cpu") == 0) - event->args.config_pair.event = 0x1000004A9ULL; // 0x1000004A0ULL ? - - // L2 Frontend (cmask=0x6 add 0x06000000) - else if (strcmp(name, "@zen4_fe_lat") == 0) - event->args.config_pair.event = 0x1060001A9ULL; // 0x1060001A0ULL ? - else if (strcmp(name, "@zen4_fe_tot") == 0) - event->args.config_pair.event = 0x1000001A9ULL; // 0x1000001A0ULL ? - - // L2 Bad Speculation + Retiring - else if (strcmp(name, "@zen4_bs_misp") == 0) - event->args.config_pair.event = 0x0008000C1ULL; // 0x00C3; // ex_ret_brn_misp - else if (strcmp(name, "@zen4_bs_resync") == 0) - event->args.config_pair.event = 0x0002000C1ULL; // 0x0091; // bp_de_redirect - else if (strcmp(name, "@zen4_ret_micro") == 0) - event->args.config_pair.event = 0x0001000C1ULL; // 0x1000000C2ULL; // ex_ret_ucode_ops - else - return 1; - - return 0; +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 + // Modern Intel (Ice Lake / Raptor Lake) else if (strncmp(name, "@icl_", 5) == 0) { - event->mode = PERF_ARG_GENERIC; - event->args.config_pair.type = PERF_TYPE_RAW; - - if (strcmp(name, "@icl_slots") == 0) - event->args.config_pair.event = 0x0400; - // TopDownL1 - else if (strcmp(name, "@icl_retiring") == 0) - event->args.config_pair.event = 0x8000; - else if (strcmp(name, "@icl_bad_spec") == 0) - event->args.config_pair.event = 0x8100; - else if (strcmp(name, "@icl_fe_bound") == 0) - event->args.config_pair.event = 0x8200; - else if (strcmp(name, "@icl_be_bound") == 0) - event->args.config_pair.event = 0x8300; - // TopDownL2 - else if (strcmp(name, "@icl_heavy_ops") == 0) - event->args.config_pair.event = 0x8400; - else if (strcmp(name, "@icl_br_mispredict") == 0) - event->args.config_pair.event = 0x8500; - else if (strcmp(name, "@icl_fetch_lat") == 0) - event->args.config_pair.event = 0x8600; - else if (strcmp(name, "@icl_mem_bound") == 0) - event->args.config_pair.event = 0x8700; - // TopdownL3_Mem - else if (strcmp(name, "@icl_cyc") == 0) - event->args.config_pair.event = 0x003c; - else if (strcmp(name, "@icl_stalls_mem_any") == 0) - event->args.config_pair.event = 0x140014a3; - else if (strcmp(name, "@icl_stalls_l1d_miss") == 0) - event->args.config_pair.event = 0x0c000ca3; - else if (strcmp(name, "@icl_stalls_l2_miss") == 0) - event->args.config_pair.event = 0x050005a3; - else if (strcmp(name, "@icl_stalls_l3_miss") == 0) - event->args.config_pair.event = 0x060006a3; - else if (strcmp(name, "@icl_bound_on_stores") == 0) - event->args.config_pair.event = 0x40a6; - else - return 1; - - return 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; + return -1; // unknow prefix } int get_perf_event_config(const char *name, perf_event_args_t *event) { From a6defa809258a7317d7eb483e732df1937fad0ca Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 11 Jun 2026 10:12:59 +0200 Subject: [PATCH 44/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 40 +++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index b40dc54f..ab81e37e 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -38,7 +38,7 @@ def _(mo): > To make hardware counters available to userspace applications (ring 3), run this in your terminal: > ```bash > sudo sysctl kernel.perf_event_paranoid=-1 - > echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog + > echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog # not needed on ARM > ``` """) return @@ -60,8 +60,8 @@ def _(mo): @app.cell def _(mo): # UI sliders - tile_i_ui = mo.ui.slider(start=8, stop=1024, step=8, value=16, label="Tile I (Rows)") - tile_j_ui = mo.ui.slider(start=8, stop=1024, step=8, value=16, label="Tile J (Cols)") + tile_i_ui = mo.ui.slider(start=16, stop=1024, step=16, value=16, label="Tile I (Rows)") + tile_j_ui = mo.ui.slider(start=16, stop=1024, step=16, value=16, label="Tile J (Cols)") unroll_ui = mo.ui.slider(start=1, stop=32, step=1, value=2, label="Unroll factor") return tile_i_ui, tile_j_ui, unroll_ui @@ -102,7 +102,13 @@ def _(mo): ) sched = scheduler.schedule() - compiler = backend.get_compiler(dump_file="matmul_mlir", shared_lib=True) + compiler = backend.get_compiler( + dump_file="matmul_mlir", + print_source_ir=False, + print_transformed_ir=False, + print_assembly=False, + shared_lib=True + ) module = compiler.compile(sched) ''' descript_editor = mo.ui.code_editor( @@ -366,10 +372,36 @@ def _(btn_l2, mo): * **Core Bound:** Lack of execution units (ALU/FPU) or data dependencies between instructions. * **Memory Bound:** Execution Units are starved because of non-completed in-flight memory demand loads. + **Analyzing Retiring:** + If **🟢 Retiring: Light Ops** is unusually high, it often indicates inefficient vectorization. + This typically occurs when matrix dimensions are not perfectly divisible by your tile sizes, + forcing the compiler to generate scalar instructions for the remaining elements (loop tails). + You can verify if the backend successfully leverages wide vector registers (like `ymm` or `zmm`) + by inspecting the generated assembly. + + {btn_l2} """) return +@app.cell +def _(mo): + check_asm_content = mo.md( + """ + - Set the flag **print_assembly** to **True** in the code editor cell. + - The output will be in the Marimo's server terminal the next compilation of the module. + """ + ) + + check_asm_msg = mo.accordion({ + "💡 How to see assembly output": check_asm_content + }) + return (check_asm_msg,) + +@app.cell +def _(check_asm_msg): + check_asm_msg + return @app.cell def _(btn_l2, exec_error, mo, module, platform): From d26a340549ec3416731a3bf32265d8d1a0f8db55 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Thu, 11 Jun 2026 12:37:28 +0200 Subject: [PATCH 45/92] [TMA]aarch64 support for TopdownL1,TopdownL2 (untested) --- .../csrcs/runtimes/host/perf_event_linux.c | 16 +++ src/xtc/csrcs/runtimes/host/perf_metrics.c | 119 +++++++++++++++++- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 0b549346..bb31c19f 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -281,6 +281,22 @@ static const pmu_event_def_t zen4_raw_events[] = { { "@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/master/arch/x86/events/intel/core.c diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index dffe2516..3f9bcd21 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -547,6 +547,110 @@ static void compute_amd_zen4_tma_l2(const double *raw, double *final) { } +// === 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; + } +} // === Core logic === @@ -635,12 +739,15 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { // else if (uarch == AMD_ZEN_3) ... } else if (detect_if_arm()) { - fprintf(stderr,"[DEBUG] ARM detected\n"); - // todo - // fopen /sys/devices/system/cpu/cpu0/regs/identification/midr_el1 - // 0x410fd0c0 == Cortex-X1 - // 0x610f2200 == Apple M1 - return 0; + fprintf(stderr,"[DEBUG] ARM AArch64 detected\n"); + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 5; + out_resolver->hw_events = arm_tma_l1_events; + out_resolver->num_results = 4; + out_resolver->num_passes = 1; + out_resolver->events_per_pass = arm_l1_passes; + out_resolver->compute_formula = compute_arm_tma_l1; + return 1; } } else if (strcmp(metric_name, "TopdownL2") == 0) { From d22d57fd8446b9a67c8b74296c49a726a6d7f168 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 12 Jun 2026 12:02:57 +0200 Subject: [PATCH 46/92] [TMA]rename arg tmu_counters to hw_counters --- docs/tutorials/hw_counters_introduction.py | 28 +++++++-------- src/xtc/schedules/ttile/scheme_to_xtc.py | 34 +++++++++---------- .../targets/accelerator/gpu/GPUEvaluator.py | 4 +-- .../targets/accelerator/mppa/MppaEvaluator.py | 4 +-- src/xtc/targets/host/HostEvaluator.py | 4 +-- src/xtc/utils/evaluation.py | 14 ++++---- .../evaluation/test_matmul_pmu_counters.py | 2 +- .../test_matmul_pmu_counters_gpu.py | 2 +- .../test_matmul_pmu_counters_mppa.py | 2 +- .../evaluation/test_matmul_tma_counters.py | 2 +- tests/pytest/ttile/test_scheme_to_xtc.py | 2 +- 11 files changed, 49 insertions(+), 49 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index ab81e37e..f986445f 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -278,7 +278,7 @@ def _(btn_pmu, exec_error, mo, module): else: pmu_counters = ["cycles", "instructions", "cache_access", "cache_misses", "branches", "branches_misses"] - evaluator_pmu = module.get_evaluator(validate=True, pmu_counters=pmu_counters) + evaluator_pmu = module.get_evaluator(validate=True, hw_counters=pmu_counters) results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() _pmu_data = [{"Counter": c, "Value": "Fallback needed" if v == -1.0 else str(int(v))} for c, v in zip(pmu_counters, results_pmu)] @@ -315,7 +315,7 @@ def _(btn_l1, exec_error, mo, module, platform): l1_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: tma_l1_counters = ["TopdownL1"] if platform == "linux" else [] - evaluator_l1 = module.get_evaluator(validate=False, pmu_counters=tma_l1_counters) + evaluator_l1 = module.get_evaluator(validate=False, hw_counters=tma_l1_counters) results_l1, code_l1, error_l1 = evaluator_l1.evaluate() _l1_labels = ["🟢 Retiring", "🔴 Bad Speculation", "🔵 Frontend Bound", "🟣 Backend Bound"] @@ -411,7 +411,7 @@ def _(btn_l2, exec_error, mo, module, platform): l2_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") else: tma_l2_counters = ["TopdownL2"] if platform == "linux" else [] - evaluator_l2 = module.get_evaluator(validate=False, pmu_counters=tma_l2_counters) + evaluator_l2 = module.get_evaluator(validate=False, hw_counters=tma_l2_counters) results_l2, code_l2, error_l2 = evaluator_l2.evaluate() _l2_labels = [ @@ -562,16 +562,16 @@ def _(mo): ### Level 2 (8 metrics) **Array Order:** `[Light Ops, Heavy Ops, Machine Clears, Branch Mispredicts, Fetch Bandwidth, Fetch Latency, Core Bound, Memory Bound]` - | Index | Category | Sub-Metric | - |---|---|---| - | `[0]` | 🟢 `Retiring` | `light_operations` | - | `[1]` | 🟢 `Retiring` | `heavy_operations` | - | `[2]` | 🔴 `Bad Speculation` | `machine_clears` | - | `[3]` | 🔴 `Bad Speculation` | `branch_mispredicts` | - | `[4]` | 🔵 `Frontend Bound` | `fetch_bandwidth` | - | `[5]` | 🔵 `Frontend Bound` | `fetch_latency` | - | `[6]` | 🟣 `Backend Bound` | `core_bound` | - | `[7]` | 🟣 `Backend 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). | ### Level 3 (26 metrics) Deep dive into L2 categories. Key metrics returned in the array include: @@ -630,7 +630,7 @@ def _(btn_sandbox, mo, module, sandbox_editor): custom_counters = ast.literal_eval(sandbox_editor.value) if not isinstance(custom_counters, list): raise ValueError("Input must be a Python list.") - evaluator_sb = module.get_evaluator(validate=False, pmu_counters=custom_counters) + evaluator_sb = module.get_evaluator(validate=False, hw_counters=custom_counters) results_sb, code_sb, err_sb = evaluator_sb.evaluate() output_lines = [f"**Execution Code:** `{code_sb}`\n"] diff --git a/src/xtc/schedules/ttile/scheme_to_xtc.py b/src/xtc/schedules/ttile/scheme_to_xtc.py index 96a2c3bb..8c663238 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 7da994e5..55f545df 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 27fd8594..47cca8f9 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 a04ae428..d9db38cd 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 c552d550..58e90f2c 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -174,7 +174,7 @@ def validate_outputs( 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, @@ -184,11 +184,11 @@ def evaluate_performance( cfunc = CFunc(func) args_tuples = cfunc.args_tuples([*parameters[0], *parameters[1]]) - if len(pmu_counters) > 0: + if len(hw_counters) > 0: values_num = 0 - for counter in pmu_counters: + for counter in hw_counters: values_num += DERIVED_METRICS_SIZES.get(counter, 1) - # FIXME check if the PMU counters are supported by the target + # FIXME check if the HW counters are supported by the target else: values_num = 1 print(f"[DEBUG] values_num : {values_num}") @@ -202,7 +202,7 @@ def evaluate_performance( ) runtime.evaluate_packed_perf( results_array, - pmu_counters, + hw_counters, repeat, number, min_repeat_ms, @@ -217,7 +217,7 @@ def evaluate_performance( ) runtime.evaluate_perf( results_array, - pmu_counters, + hw_counters, repeat, number, min_repeat_ms, @@ -231,7 +231,7 @@ def evaluate_performance( failed_counters = [] current_idx = 0 - for counter in pmu_counters: + for counter in hw_counters: size = DERIVED_METRICS_SIZES.get(counter, 1) chunk = eval_results[current_idx : current_idx + size] diff --git a/tests/filecheck/evaluation/test_matmul_pmu_counters.py b/tests/filecheck/evaluation/test_matmul_pmu_counters.py index 51c7e5d2..fd55b76e 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 ab671dea..c6e04525 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 66a7376c..c71de8e1 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 index 02f7344b..e5571f60 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -64,7 +64,7 @@ evaluator = module.get_evaluator( validate=True, - pmu_counters=hw_counters, + hw_counters=hw_counters, ) results, code, error = evaluator.evaluate() print(f"{'CODE':<25}: {code}") diff --git a/tests/pytest/ttile/test_scheme_to_xtc.py b/tests/pytest/ttile/test_scheme_to_xtc.py index 455ae21f..82786b29 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()) From 6282ca359714c0e903f636f4b3a582bd5e04718c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 12 Jun 2026 15:24:19 +0200 Subject: [PATCH 47/92] [TMA]add missing license, fix banwords --- src/xtc/csrcs/runtimes/host/perf_event_linux.c | 4 ++-- src/xtc/csrcs/runtimes/host/perf_metrics.c | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index bb31c19f..f986e7c9 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -299,8 +299,8 @@ static const pmu_event_def_t arm_raw_events[] = { /* * Source - * Intel : https://github.com/torvalds/linux/blob/master/arch/x86/events/intel/core.c - * AMD : https://github.com/torvalds/linux/blob/master/arch/x86/events/amd/core.c + * 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 diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 3f9bcd21..ddd77476 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -1,3 +1,7 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2024-2026 The XTC Project Authors + */ #include #include #include From 99778d5d41d76fb0e67b98c767021f4d18ab1129 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 12 Jun 2026 16:59:54 +0200 Subject: [PATCH 48/92] [TMA]fix expected output --- tests/filecheck/evaluation/test_matmul_tma_counters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index e5571f60..efca7727 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -67,9 +67,9 @@ hw_counters=hw_counters, ) results, code, error = evaluator.evaluate() -print(f"{'CODE':<25}: {code}") -print(f"{'counters':<25}: {hw_counters}") -print(f"{'results':<25}: {[round(x, 2) for x in results]}") +print(f"CODE: {code}") +print(f"{'counters'}: {hw_counters}") +print(f"{'results'}: {[round(x, 2) for x in results]}") use_colors = sys.stdout.isatty() From ae514d28d788ec168ef21d59d79a592aa8009826 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 12 Jun 2026 17:00:51 +0200 Subject: [PATCH 49/92] [TMA]fix evaluate_perf call using the new interface --- src/xtc/runtimes/accelerator/mppa/MppaDevice.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/xtc/runtimes/accelerator/mppa/MppaDevice.py b/src/xtc/runtimes/accelerator/mppa/MppaDevice.py index 33a33801..b8ee7027 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( From 77c07775d304f8a6dbbcb48e08eae8a700ae1254 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 12 Jun 2026 17:02:35 +0200 Subject: [PATCH 50/92] [TMA]Disable debug messages, fix perf fallback's return value --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 22 ++++++++--------- src/xtc/utils/evaluation.py | 28 +++++++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index ddd77476..33775b68 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -7,7 +7,7 @@ #include #include #include -#include +//#include #include #include "perf_metrics.h" @@ -690,11 +690,11 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { if (strcmp(metric_name, "TopdownL1") == 0) { if (detect_if_intel()) { - fprintf(stderr,"[DEBUG] INTEL detected\n"); + //fprintf(stderr,"[DEBUG] INTEL detected\n"); intel_arch_t uarch = detect_intel_microarchitecture(); if (uarch == INTEL_SKYLAKE_CASCADE) { - fprintf(stderr,"[DEBUG] Old Intel detected\n"); + //fprintf(stderr,"[DEBUG] Old Intel detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 5; out_resolver->hw_events = skl_tma_l1_events; @@ -704,7 +704,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->compute_formula = compute_skl_tma_l1; return 1; } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - fprintf(stderr,"[DEBUG] Modern Intel detected\n"); + //fprintf(stderr,"[DEBUG] Modern Intel detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 5; out_resolver->hw_events = intel_modern_tma_l1_events; @@ -719,7 +719,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { amd_arch_t uarch = detect_amd_microarchitecture(); if (uarch == AMD_ZEN_4) { - fprintf(stderr,"[DEBUG] AMD_ZEN_4 detected\n"); + //fprintf(stderr,"[DEBUG] AMD_ZEN_4 detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 4; out_resolver->hw_events = amd_zen4_tma_l1_events; @@ -730,7 +730,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } else if (uarch == AMD_ZEN_1_2) { - fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected (unsuported)\n"); + //fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected (unsuported)\n"); //out_resolver->is_supported = 1; //out_resolver->num_hw_events = 4; //out_resolver->hw_events = amd_zen4_tma_l1_events; // Todo change to zen1 @@ -743,7 +743,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { // else if (uarch == AMD_ZEN_3) ... } else if (detect_if_arm()) { - fprintf(stderr,"[DEBUG] ARM AArch64 detected\n"); + //fprintf(stderr,"[DEBUG] ARM AArch64 detected\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 5; out_resolver->hw_events = arm_tma_l1_events; @@ -759,7 +759,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { intel_arch_t uarch = detect_intel_microarchitecture(); if (uarch == INTEL_SKYLAKE_CASCADE) { - fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L2 (Multi-Pass)\n"); + //fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L2 (Multi-Pass)\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 12; out_resolver->hw_events = skl_tma_l2_events; @@ -770,7 +770,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { return 1; } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - fprintf(stderr,"[DEBUG] INTEL_ICELAKE_SAPPHIRE L2\n"); + //fprintf(stderr,"[DEBUG] INTEL_ICELAKE_SAPPHIRE L2\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 9; out_resolver->hw_events = intel_modern_tma_l2_events; @@ -785,7 +785,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { else if (detect_if_amd()) { // L2 amd amd_arch_t uarch = detect_amd_microarchitecture(); if (uarch == AMD_ZEN_4) { - fprintf(stderr,"[DEBUG] AMD_ZEN_4 L2 (Multi-Pass)\n"); + //fprintf(stderr,"[DEBUG] AMD_ZEN_4 L2 (Multi-Pass)\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 9; out_resolver->hw_events = amd_zen4_tma_l2_events; @@ -802,7 +802,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { if (detect_if_intel()) { intel_arch_t uarch = detect_intel_microarchitecture(); if (uarch == INTEL_SKYLAKE_CASCADE) { - fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L3_Mem (Multi-Pass)\n"); + //fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L3_Mem (Multi-Pass)\n"); out_resolver->is_supported = 1; out_resolver->num_hw_events = 7; out_resolver->hw_events = skl_tma_l3_mem_events; diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index 58e90f2c..e1faa290 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -191,8 +191,13 @@ def evaluate_performance( # FIXME check if the HW counters are supported by the target else: values_num = 1 - print(f"[DEBUG] values_num : {values_num}") + # 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] @@ -225,7 +230,7 @@ def evaluate_performance( args_array, len(args_array), ) - print(f"[DEBUG] results: {[round(x, 2) for x in results_array]}") + # print(f"[DEBUG] results: {[round(x, 2) for x in results_array]}") eval_results = [float(x) for x in results_array] failed_counters = [] @@ -248,7 +253,7 @@ def evaluate_performance( perf_path = shutil.which("perf") if not perf_path: - return (eval_results, 1, "perf tool not found in PATH") + return (eval_results, 0, "perf tool not found in PATH") my_pid = str(os.getpid()) @@ -284,7 +289,7 @@ def evaluate_performance( cmd.extend(["-M", ",".join(perf_metrics)]) try: - print("[DEBUG] Starting perf...") + # print("[DEBUG] Starting perf...") perf_proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) @@ -294,7 +299,7 @@ def evaluate_performance( # Rerun evaluation without HW counters to generate activity for perf dummy_results = (ctypes.c_double * repeat)() if cfunc.is_packed: - print("[DEBUG] Rerun packed (perf)...") + # print("[DEBUG] Rerun packed (perf)...") _ = runtime.evaluate_packed_perf( dummy_results, [], @@ -307,7 +312,12 @@ def evaluate_performance( len(args_tuples), ) else: - print("[DEBUG] Rerun not packed (perf)...") + assert args_array_packed is not None + assert args_codes_packed is not None + # print("[DEBUG] Rerun not packed (perf)...") + args_array = (ctypes.c_voidp * len(args_tuples))( + *[arg[0] for arg in args_tuples] + ) runtime.evaluate_perf( dummy_results, [], @@ -319,7 +329,7 @@ def evaluate_performance( len(args_array), ) - print("[DEBUG] Stopping perf...") + # print("[DEBUG] Stopping perf...") perf_proc.send_signal(signal.SIGINT) _, stderr_output = perf_proc.communicate(timeout=5.0) @@ -335,8 +345,8 @@ def evaluate_performance( return (eval_results, 0, formatted_fallback_output) except Exception as e: - print(f"[DEBUG] Fallback perf stat failed : {e}") - return (eval_results, 1, f"Fallback perf stat failed: {e}") + # print(f"[DEBUG] Fallback perf stat failed : {e}") + return (eval_results, 0, f"Fallback perf stat failed: {e}") # Return API result return (eval_results, 0, "") From 2c4f26dc5b38e29c7fbcbb2ae4d5f1fb12d6a032 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 12 Jun 2026 17:12:09 +0200 Subject: [PATCH 51/92] [TMA]Remove unused script --- activate_hw_counters.sh | 2 -- 1 file changed, 2 deletions(-) delete mode 100755 activate_hw_counters.sh diff --git a/activate_hw_counters.sh b/activate_hw_counters.sh deleted file mode 100755 index b4517a7b..00000000 --- a/activate_hw_counters.sh +++ /dev/null @@ -1,2 +0,0 @@ -sudo sysctl kernel.perf_event_paranoid=-1 -echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog From 1cc489342e918fa68a5e5229c3d1be4e3fbf4798 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 15 Jun 2026 09:10:23 +0200 Subject: [PATCH 52/92] [TMA]Put Linux-specific code between ifdefs --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 33775b68..0aedaa5e 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -4,7 +4,11 @@ */ #include #include + +#ifdef __linux__ #include +#endif //__linux__ + #include #include //#include @@ -687,6 +691,7 @@ static void compute_arm_tma_l2(const double *raw, double *final) { * */ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { +#ifdef __linux__ if (strcmp(metric_name, "TopdownL1") == 0) { if (detect_if_intel()) { @@ -825,6 +830,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { } return 0; } +#endif //__linux__ // Unsuported hardware / metric or the event is a pmu return 0; From 00db8fc10347bb25a7a5dadb0094469969f20bd8 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 15 Jun 2026 10:07:02 +0200 Subject: [PATCH 53/92] [TMA]Put Linux-specific code between ifdefs --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 0aedaa5e..ac40249f 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -4,22 +4,20 @@ */ #include #include - -#ifdef __linux__ -#include -#endif //__linux__ - #include #include //#include -#include #include "perf_metrics.h" +#ifdef __linux__ +#include +#endif //__linux__ #if defined(__x86_64__) || defined(__i386__) #define ARCH_IS_X86 1 #include + #include #else #define ARCH_IS_X86 0 #endif @@ -830,10 +828,14 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { } return 0; } -#endif //__linux__ - // Unsuported hardware / metric or the event is a pmu return 0; + #else + (void)metric_name; + (void)out_resolver; + return 0; + #endif //__linux__ + } int get_perf_metric_results_count(const char *metric_name) { From d91b049508150b17df4f437eedd46e336aebaa74 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 15 Jun 2026 10:20:47 +0200 Subject: [PATCH 54/92] [TMA]Put Linux-specific code between ifdefs --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index ac40249f..4a0f3ae1 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -10,9 +10,6 @@ #include "perf_metrics.h" -#ifdef __linux__ -#include -#endif //__linux__ #if defined(__x86_64__) || defined(__i386__) #define ARCH_IS_X86 1 @@ -31,6 +28,8 @@ #if ARCH_IS_X86 #include +#ifdef __linux__ +#include static void get_cpu_family_model(int *family, int *model) { unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; @@ -273,22 +272,22 @@ static void compute_skl_tma_l2(const double *raw, double *final) { double slots_p3 = raw[8] * 4.0; if (slots_p1 > 0 && slots_p2 > 0 && slots_p3 > 0) { - // 1. Frontend + // Frontend double fe_bound = raw[3] / slots_p1; double fetch_lat = (raw[5] + raw[6]) / slots_p2; double fetch_bw = fe_bound - fetch_lat; - // 2. Retiring + // Retiring double retiring = raw[7] / slots_p2; double heavy_ops = raw[9] / slots_p3; double light_ops = retiring - heavy_ops; - // 3. Bad Speculation + // Bad Speculation double br_misp = raw[10] / slots_p3; double m_clears = raw[11] / slots_p3; double bad_spec = br_misp + m_clears; - // 4. Backend + // Backend double mem_bound = raw[1] / slots_p1; double core_bound = raw[2] / slots_p1; @@ -658,6 +657,9 @@ static void compute_arm_tma_l2(const double *raw, double *final) { } } +#endif //__linux__ + + // === Core logic === From b669d7a95e5b6da9a7bd5c7c10db05171629ece2 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 15 Jun 2026 10:33:05 +0200 Subject: [PATCH 55/92] Fix new tma test for unsupported arch --- .../evaluation/test_matmul_tma_counters.py | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index efca7727..93938f87 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -71,53 +71,53 @@ print(f"{'counters'}: {hw_counters}") print(f"{'results'}: {[round(x, 2) for x in results]}") - -use_colors = sys.stdout.isatty() - -RED = "\033[91m" if use_colors else "" -ORANGE = "\033[38;5;208m" if use_colors else "" -YELLOW = "\033[93m" if use_colors else "" -GREEN = "\033[92m" if use_colors else "" -MAGENTA = "\033[95m" if use_colors else "" -RESET = "\033[0m" if use_colors else "" - -def get_c(val): - if val > 90: return RED - if val > 75: return ORANGE - if val > 50: return YELLOW - if val > 10: return GREEN - # < 10 white - if val == -1: return MAGENTA - return RESET - -w = 25 - -print("-" * (w + 10)) - -# L1 Metrics -print(f"{'L1 Retiring':<{w}}: {get_c(results[0])}{results[0]:.2f}{RESET}") -print(f"{'L1 Bad speculation':<{w}}: {get_c(results[1])}{results[1]:.2f}{RESET}") -print(f"{'L1 Frontend bound':<{w}}: {get_c(results[2])}{results[2]:.2f}{RESET}") -print(f"{'L1 Backend bound':<{w}}: {get_c(results[3])}{results[3]:.2f}{RESET}") - -print("") -# L2 Metrics -print(f"{'L2 Light ops':<{w}}: {get_c(results[4])}{results[4]:.2f}{RESET}") -print(f"{'L2 Heavy ops':<{w}}: {get_c(results[5])}{results[5]:.2f}{RESET}") -print(f"{'L2 Machine clear':<{w}}: {get_c(results[6])}{results[6]:.2f}{RESET}") -print(f"{'L2 Branch misspredict':<{w}}: {get_c(results[7])}{results[7]:.2f}{RESET}") -print(f"{'L2 Fetch bandwidth':<{w}}: {get_c(results[8])}{results[8]:.2f}{RESET}") -print(f"{'L2 Fetch latency':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") -print(f"{'L2 Core bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") -print(f"{'L2 Memory bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") - -print("") -# L3 Memory Metrics -print(f"{'L3 L1 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") -print(f"{'L3 L2 bound':<{w}}: {get_c(results[13])}{results[13]:.2f}{RESET}") -print(f"{'L3 L3 bound':<{w}}: {get_c(results[14])}{results[14]:.2f}{RESET}") -print(f"{'L3 DRAM bound':<{w}}: {get_c(results[15])}{results[15]:.2f}{RESET}") -print(f"{'L3 store bound':<{w}}: {get_c(results[16])}{results[16]:.2f}{RESET}") +if len(results) >= 17: + use_colors = sys.stdout.isatty() + + RED = "\033[91m" if use_colors else "" + ORANGE = "\033[38;5;208m" if use_colors else "" + YELLOW = "\033[93m" if use_colors else "" + GREEN = "\033[92m" if use_colors else "" + MAGENTA = "\033[95m" if use_colors else "" + RESET = "\033[0m" if use_colors else "" + + def get_c(val): + if val > 90: return RED + if val > 75: return ORANGE + if val > 50: return YELLOW + if val > 10: return GREEN + # < 10 white + if val == -1: return MAGENTA + return RESET + + w = 25 + + print("-" * (w + 10)) + + # L1 Metrics + print(f"{'L1 Retiring':<{w}}: {get_c(results[0])}{results[0]:.2f}{RESET}") + print(f"{'L1 Bad speculation':<{w}}: {get_c(results[1])}{results[1]:.2f}{RESET}") + print(f"{'L1 Frontend bound':<{w}}: {get_c(results[2])}{results[2]:.2f}{RESET}") + print(f"{'L1 Backend bound':<{w}}: {get_c(results[3])}{results[3]:.2f}{RESET}") + + print("") + # L2 Metrics + print(f"{'L2 Light ops':<{w}}: {get_c(results[4])}{results[4]:.2f}{RESET}") + print(f"{'L2 Heavy ops':<{w}}: {get_c(results[5])}{results[5]:.2f}{RESET}") + print(f"{'L2 Machine clear':<{w}}: {get_c(results[6])}{results[6]:.2f}{RESET}") + print(f"{'L2 Branch misspredict':<{w}}: {get_c(results[7])}{results[7]:.2f}{RESET}") + print(f"{'L2 Fetch bandwidth':<{w}}: {get_c(results[8])}{results[8]:.2f}{RESET}") + print(f"{'L2 Fetch latency':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") + print(f"{'L2 Core bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") + print(f"{'L2 Memory bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") + + print("") + # L3 Memory Metrics + print(f"{'L3 L1 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") + print(f"{'L3 L2 bound':<{w}}: {get_c(results[13])}{results[13]:.2f}{RESET}") + print(f"{'L3 L3 bound':<{w}}: {get_c(results[14])}{results[14]:.2f}{RESET}") + print(f"{'L3 DRAM bound':<{w}}: {get_c(results[15])}{results[15]:.2f}{RESET}") + print(f"{'L3 store bound':<{w}}: {get_c(results[16])}{results[16]:.2f}{RESET}") From 51814bb463a4a7f6b864490e23ac83fb78ded86d Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 15 Jun 2026 15:05:32 +0200 Subject: [PATCH 56/92] [TMA]TopdownL3 skylake support (deducted metrics missing) --- .../csrcs/runtimes/host/perf_event_linux.c | 26 ++- src/xtc/csrcs/runtimes/host/perf_metrics.c | 158 +++++++++++++++++- 2 files changed, 179 insertions(+), 5 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index f986e7c9..de872441 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -243,7 +243,31 @@ static const pmu_event_def_t skl_raw_events[] = { { "@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) } + { "@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) }, + // 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) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 4a0f3ae1..ae076c09 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -356,6 +356,139 @@ static void compute_skl_tma_l3_mem(const double *raw, double *final) { } } +static const int skl_l3_passes[] = {5, 5, 5, 5, 5, 5, 5}; + +static const char *skl_tma_l3_events[] = { + // Pass 1: Memory Bounds + "@skl_slots", + "@skl_stalls_mem_any", + "@skl_stalls_l1d_miss", + "@skl_stalls_l2_miss", + "@skl_stalls_l3_miss", + + // Pass 2: Execution Stalls + "@skl_slots", + "@skl_divider_active", + "@skl_scoreboard", + "@skl_bound_on_stores", + "@skl_core_stalls", + + // Pass 3: Frontend Latency + "@skl_slots", + "@skl_icache_miss", + "@skl_itlb_miss", + "@skl_clear_resteer", + "@skl_lcp", + + // Pass 4: Frontend Bandwidth + "@skl_slots", + "@skl_dsb2mite", + "@skl_ms_switches", + "@skl_idq_mite", + "@skl_idq_dsb", + + // Pass 5: Instruction Mix + "@skl_slots", + "@skl_idq_ms", + "@skl_macro_fused", + "@skl_mem_inst", + "@skl_br_inst", + + // Pass 6: Floating Point 1 (Scalar & 128b) + "@skl_slots", + "@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", + "@skl_fp_256_s", + "@skl_fp_256_d", + "@skl_fp_512_s", + "@skl_fp_512_d" +}; + +static void compute_skl_tma_l3(const double *raw, double *final) { + // Extraction des cycles (raw[0], raw[5], raw[10], raw[15], raw[20], raw[25], raw[30]) + 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]; + + assert(cyc_p1 != cyc_p2 != cyc_p3 != cyc_p4 != cyc_p5 != cyc_p6 != cyc_p7 != 0); + if (cyc_p1 > 0 && cyc_p2 > 0 && cyc_p3 > 0 && cyc_p4 > 0 && cyc_p5 > 0 && cyc_p6 > 0 && cyc_p7 > 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; + + // 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] = 0.0; // few_uops + 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] = 0.0; // other light + final[20] = 0.0; // other misp + final[21] = 0.0; // nukes + final[22] = 0.0; // pmm (Intel Optane DC, deprecated 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 === @@ -515,7 +648,7 @@ static void compute_amd_zen4_tma_l2(const double *raw, double *final) { double slots_p2 = raw[4] * 6.0; if (slots_p1 > 0 && slots_p2 > 0) { - // Backend Bound + // Backend Bound double be_mem = raw[1] / slots_p1; double be_cpu = raw[2] / slots_p1; @@ -525,11 +658,11 @@ static void compute_amd_zen4_tma_l2(const double *raw, double *final) { double fe_bw = fe_tot - fe_lat; if (fe_bw < 0.0) fe_bw = 0.0; - // 3. Bad Speculation + // Bad Speculation double bs_misp = raw[6] / slots_p2; double bs_clear = raw[7] / slots_p2; - // 4. Retiring + // Retiring double ret_heavy = raw[8] / slots_p2; double total_retiring = 1.0 - (be_mem + be_cpu + fe_tot + bs_misp + bs_clear); @@ -803,6 +936,23 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { } return 0; } + else if (strcmp(metric_name, "TopdownL3") == 0) { + if (detect_if_intel()) { + intel_arch_t uarch = detect_intel_microarchitecture(); + + if (uarch == INTEL_SKYLAKE_CASCADE) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 35; + out_resolver->hw_events = skl_tma_l3_events; + out_resolver->num_results = 26; + out_resolver->num_passes = 7; + out_resolver->events_per_pass = skl_l3_passes; + out_resolver->compute_formula = compute_skl_tma_l3; + return 1; + } + } + return 0; + } else if (strcmp(metric_name, "TopdownL3_Mem") == 0) { if (detect_if_intel()) { intel_arch_t uarch = detect_intel_microarchitecture(); @@ -829,7 +979,7 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { } } return 0; - } + } // Unsuported hardware / metric or the event is a pmu return 0; #else From 385ab606769b48190d09bb48f082aad5faf6e78f Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 15 Jun 2026 17:12:45 +0200 Subject: [PATCH 57/92] [TMA]TopdownL3 skylake full support --- .../csrcs/runtimes/host/perf_event_linux.c | 1 + src/xtc/csrcs/runtimes/host/perf_metrics.c | 76 ++++++++++++++----- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index de872441..cb565da2 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -259,6 +259,7 @@ static const pmu_event_def_t skl_raw_events[] = { { "@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) }, diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index ae076c09..8cea28ee 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -356,67 +356,73 @@ static void compute_skl_tma_l3_mem(const double *raw, double *final) { } } -static const int skl_l3_passes[] = {5, 5, 5, 5, 5, 5, 5}; +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", + "@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", + "@skl_slots", // raw[5] "@skl_divider_active", "@skl_scoreboard", "@skl_bound_on_stores", "@skl_core_stalls", // Pass 3: Frontend Latency - "@skl_slots", + "@skl_slots", // raw[10] "@skl_icache_miss", "@skl_itlb_miss", "@skl_clear_resteer", "@skl_lcp", // Pass 4: Frontend Bandwidth - "@skl_slots", + "@skl_slots", // raw[15] "@skl_dsb2mite", "@skl_ms_switches", "@skl_idq_mite", "@skl_idq_dsb", // Pass 5: Instruction Mix - "@skl_slots", + "@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", + "@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", + "@skl_slots", // raw[30] "@skl_fp_256_s", "@skl_fp_256_d", "@skl_fp_512_s", - "@skl_fp_512_d" + "@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) { - // Extraction des cycles (raw[0], raw[5], raw[10], raw[15], raw[20], raw[25], raw[30]) 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_p7 = raw[30]; double cyc_p8 = raw[35]; - assert(cyc_p1 != cyc_p2 != cyc_p3 != cyc_p4 != cyc_p5 != cyc_p6 != cyc_p7 != 0); - if (cyc_p1 > 0 && cyc_p2 > 0 && cyc_p3 > 0 && cyc_p4 > 0 && cyc_p5 > 0 && cyc_p6 > 0 && cyc_p7 > 0) { + 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; @@ -451,6 +457,36 @@ static void compute_skl_tma_l3(const double *raw, double *final) { // 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 @@ -462,7 +498,7 @@ static void compute_skl_tma_l3(const double *raw, double *final) { final[2] = dram_bound * 100.0; final[3] = dsb * 100.0; final[4] = dsb2mite * 100.0; - final[5] = 0.0; // few_uops + final[5] = few_uops * 100.0; final[6] = fp_arith * 100.0; final[7] = fused * 100.0; final[8] = icache * 100.0; @@ -476,10 +512,10 @@ static void compute_skl_tma_l3(const double *raw, double *final) { final[16] = mite * 100.0; final[17] = ms_swit * 100.0; final[18] = non_fused * 100.0; - final[19] = 0.0; // other light - final[20] = 0.0; // other misp - final[21] = 0.0; // nukes - final[22] = 0.0; // pmm (Intel Optane DC, deprecated tech) + 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; @@ -942,10 +978,10 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { if (uarch == INTEL_SKYLAKE_CASCADE) { out_resolver->is_supported = 1; - out_resolver->num_hw_events = 35; + out_resolver->num_hw_events = 40; out_resolver->hw_events = skl_tma_l3_events; out_resolver->num_results = 26; - out_resolver->num_passes = 7; + out_resolver->num_passes = 8; out_resolver->events_per_pass = skl_l3_passes; out_resolver->compute_formula = compute_skl_tma_l3; return 1; From 8469be3c953c466b00d1b40bde0d99b749507f48 Mon Sep 17 00:00:00 2001 From: subel Date: Tue, 16 Jun 2026 09:19:42 +0200 Subject: [PATCH 58/92] [TMA]Support TopdownL3 for modern Intel --- .../csrcs/runtimes/host/perf_event_linux.c | 52 +++++++++++++++---- src/xtc/csrcs/runtimes/host/perf_metrics.c | 52 ++++++++++++++++--- .../evaluation/test_matmul_tma_counters.py | 16 +++--- 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index cb565da2..75752b58 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -273,22 +273,52 @@ static const pmu_event_def_t skl_raw_events[] = { // INTEL MODERN (Ice Lake, Sapphire Rapids, Alder/Raptor Lake) static const pmu_event_def_t icl_raw_events[] = { - { "@icl_slots", X86_RAW(0x00, 0x04, 0) }, - { "@icl_retiring", X86_RAW(0x00, 0x80, 0) }, - { "@icl_bad_spec", X86_RAW(0x00, 0x81, 0) }, - { "@icl_fe_bound", X86_RAW(0x00, 0x82, 0) }, - { "@icl_be_bound", X86_RAW(0x00, 0x83, 0) }, - { "@icl_heavy_ops", X86_RAW(0x00, 0x84, 0) }, - { "@icl_br_mispredict", X86_RAW(0x00, 0x85, 0) }, - { "@icl_fetch_lat", X86_RAW(0x00, 0x86, 0) }, - { "@icl_mem_bound", X86_RAW(0x00, 0x87, 0) }, - // Fallback L3 Memory events (same as Skylake) + // 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) } + { "@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) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 8cea28ee..4161e765 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -356,7 +356,7 @@ static void compute_skl_tma_l3_mem(const double *raw, double *final) { } } -static const int skl_l3_passes[] = {5, 5, 5, 5, 5, 5, 5, 5}; +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 @@ -365,28 +365,28 @@ static const char *skl_tma_l3_events[] = { "@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", @@ -423,7 +423,7 @@ static void compute_skl_tma_l3(const double *raw, double *final) { 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; @@ -487,8 +487,8 @@ static void compute_skl_tma_l3(const double *raw, double *final) { // 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 : + + // 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 @@ -618,6 +618,31 @@ static const char *intel_modern_tma_l3_mem_events[] = { "@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_core_stalls", + // 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[] = { @@ -986,7 +1011,18 @@ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { out_resolver->compute_formula = compute_skl_tma_l3; return 1; } + else if (uarch == INTEL_ICELAKE_SAPPHIRE) { + out_resolver->is_supported = 1; + out_resolver->num_hw_events = 40; + out_resolver->hw_events = intel_modern_tma_l3_events; + out_resolver->num_results = 26; + out_resolver->num_passes = 8; + out_resolver->events_per_pass = icl_l3_passes; + out_resolver->compute_formula = compute_skl_tma_l3; + return 1; + } } + return 0; } else if (strcmp(metric_name, "TopdownL3_Mem") == 0) { diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index 93938f87..ea3b006e 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -54,7 +54,7 @@ # Linux Perf counters if sys.platform == "linux": hw_counters += [ - "TopdownL1","TopdownL2", "TopdownL3_Mem" + "TopdownL1","TopdownL2", "TopdownL3" ] elif sys.platform == "darwin": # On MacOS, requires sudo to get counters @@ -73,14 +73,14 @@ if len(results) >= 17: use_colors = sys.stdout.isatty() - + RED = "\033[91m" if use_colors else "" ORANGE = "\033[38;5;208m" if use_colors else "" YELLOW = "\033[93m" if use_colors else "" GREEN = "\033[92m" if use_colors else "" MAGENTA = "\033[95m" if use_colors else "" RESET = "\033[0m" if use_colors else "" - + def get_c(val): if val > 90: return RED if val > 75: return ORANGE @@ -89,17 +89,17 @@ def get_c(val): # < 10 white if val == -1: return MAGENTA return RESET - + w = 25 - + print("-" * (w + 10)) - + # L1 Metrics print(f"{'L1 Retiring':<{w}}: {get_c(results[0])}{results[0]:.2f}{RESET}") print(f"{'L1 Bad speculation':<{w}}: {get_c(results[1])}{results[1]:.2f}{RESET}") print(f"{'L1 Frontend bound':<{w}}: {get_c(results[2])}{results[2]:.2f}{RESET}") print(f"{'L1 Backend bound':<{w}}: {get_c(results[3])}{results[3]:.2f}{RESET}") - + print("") # L2 Metrics print(f"{'L2 Light ops':<{w}}: {get_c(results[4])}{results[4]:.2f}{RESET}") @@ -110,7 +110,7 @@ def get_c(val): print(f"{'L2 Fetch latency':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") print(f"{'L2 Core bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") print(f"{'L2 Memory bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") - + print("") # L3 Memory Metrics print(f"{'L3 L1 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") From 23d7e4762a0f244a84c66e2b3421d961dd849820 Mon Sep 17 00:00:00 2001 From: subel Date: Tue, 16 Jun 2026 09:52:46 +0200 Subject: [PATCH 59/92] [TMA]Fix formula TopdownL3 for modern Intel --- src/xtc/csrcs/runtimes/host/perf_event_linux.c | 1 + src/xtc/csrcs/runtimes/host/perf_metrics.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_event_linux.c b/src/xtc/csrcs/runtimes/host/perf_event_linux.c index 75752b58..080477a2 100644 --- a/src/xtc/csrcs/runtimes/host/perf_event_linux.c +++ b/src/xtc/csrcs/runtimes/host/perf_event_linux.c @@ -273,6 +273,7 @@ static const pmu_event_def_t skl_raw_events[] = { // 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 diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 4161e765..cc5cf4a8 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -626,7 +626,7 @@ 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_core_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 From 7190552d532dbb36a11eb370518133cb8550106a Mon Sep 17 00:00:00 2001 From: subel Date: Tue, 16 Jun 2026 16:11:19 +0200 Subject: [PATCH 60/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 190 ++++++++++++++++----- 1 file changed, 150 insertions(+), 40 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index f986445f..04fe3528 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -525,31 +525,61 @@ def _(mo): """ | Microarchitecture | Supported TMA Levels | Execution Mode | |---|---|---| - | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem` | Native API | - | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2`, `TopdownL3_Mem` | Native API | + | **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 | - | **Generic Linux (`perf` tool)** | `TopdownL1` -> `TopdownL6` | System Fallback *(Multiplexed)* | + | **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 : TopdownL3_Mem return [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.* + """ + ) - *Metrics beyond L2 or unmapped architectures will automatically use the `perf` fallback.* + 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": tma_support_content + "View supported TMA architectures": mo.vstack([ + tma_support_content, + zen4_accordion, + fallback_note + ]) }) - return (supported_arch_msg,) + return (supported_arch_msg,) @app.cell def _(mo): - tma_output_content = mo.md( + l1_md = mo.md( """ - TMA hierarchically breaks down CPU bottlenecks. When the native C API evaluates `TopdownL1` or `TopdownL2`, it returns a raw array of percentages. Here is the exact index mapping: - - ### Level 1 (4 metrics) **Array Order:** `[Retiring, Bad Speculation, Frontend Bound, Backend Bound]` | Index | Metric | Description | @@ -558,8 +588,11 @@ def _(mo): | `[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. | + """ + ) - ### Level 2 (8 metrics) + 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 | @@ -572,39 +605,116 @@ def _(mo): | `[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. | + """ + ) - ### Level 3 (26 metrics) - Deep dive into L2 categories. Key metrics returned in the array include: - * **Memory Subsystem:** `l1_bound`, `l2_bound`, `l3_bound`, `dram_bound`, `store_bound`, `pmm_bound` - * **Frontend Fetch:** `dsb` (decoded uop cache), `mite` (legacy decode), `icache_misses`, `itlb_misses`, `lcp` - * **Execution & ALU:** `fp_arith`, `ports_utilization`, `divider`, `serializing_operation` - * **Retiring Details:** `memory_operations`, `fused_instructions`, `microcode_sequencer` - * **Branch Prediction:** `branch_resteers`, `other_mispredicts`, `other_nukes` - - ### Level 4 (32 metrics) - Granular details on memory behavior and port utilization. - * **Memory Bandwidth & Latency:** `mem_bandwidth`, `mem_latency`, `l1_hit_latency`, `l3_hit_latency` - * **Memory Bottlenecks:** `split_loads`, `split_stores`, `4k_aliasing`, `dtlb_load`, `dtlb_store`, `false_sharing` - * **Port Utilization:** `ports_utilized_0`, `ports_utilized_1`, `ports_utilized_2`, `ports_utilized_3m` - * **Operations:** `fp_scalar`, `fp_vector`, `nop_instructions`, `x87_use`, `cisc`, `assists` - - ### Level 5 (15 metrics) - Specific unit utilization and Translation Lookaside Buffer (TLB) deep dives. - * **Operations:** `alu_op_utilization`, `load_op_utilization`, `store_op_utilization` - * **Vector Widths:** `fp_vector_128b`, `fp_vector_256b`, `fp_vector_512b` - * **STLB:** `load_stlb_hit/miss`, `store_stlb_hit/miss` - * **Memory/Cache:** `local_mem`, `remote_mem`, `remote_cache` - - ### Level 6 (8 metrics) - * **`port_0` to `port_7`:** Fraction of cycles the CPU dispatched uops on specific hardware execution ports (e.g., ALU, Loads, Stores, Branches). + l4_md = mo.md( + """ + *Note: Returned as named attributes via the `perf` fallback.* + + | 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. | """ ) - reminder_output_msg = mo.accordion({ - "💡 Output reminder & Topdown Metrics Dictionary": tma_output_content - }) - return (reminder_output_msg,) + l5_md = mo.md( + """ + *Note: Returned as named attributes via the `perf` fallback.* + + | 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( + """ + *Note: Returned as named attributes via the `perf` fallback.* + + | 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:** Native C API returns arrays for L1, L2 and L3. L4 and beyond use the `perf` fallback output."), + mo.accordion({ + "Level 1 (4 metrics - Array output)": l1_md, + "Level 2 (8 metrics - Array output)": l2_md, + "Level 3 (26 metrics - Array output)": l3_md, + "Level 4 (32 metrics - perf fallback)": l4_md, + "Level 5 (15 metrics - perf fallback)": l5_md, + "Level 6 (8 metrics - perf fallback)": l6_md + }) + ]) + return reminder_output_msg, @app.cell def _(reminder_output_msg): From 4ef60860755bdd131d768264d1111b1eddb60c8e Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Wed, 27 May 2026 18:06:29 +0200 Subject: [PATCH 61/92] [DESCRIPT] Descript++ notebook --- docs/tutorials/descript++_notebook.py | 673 ++++++++++++++++++++++++++ 1 file changed, 673 insertions(+) create mode 100644 docs/tutorials/descript++_notebook.py diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py new file mode 100644 index 00000000..fed3270e --- /dev/null +++ b/docs/tutorials/descript++_notebook.py @@ -0,0 +1,673 @@ +import marimo + +__generated_with = "0.23.8" +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 + + 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="MLIR", 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 + + 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, + render_editor_output, + run_exploration, + start_streaming_execution, + traceback, + ) + + +@app.cell +def _(mo): + mo.md(r""" + # Descript + + XTC allows you to descripte 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. + + `descript` also has syntax for loop spliting. + + For example: + ``` + 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""" + **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_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) + + # Schedule specification + schedule_spec = """ + i: parallelize + k: + j: + i#8: unroll + j#16: vectorize + """ + + # Compile + scheduler = backend.get_scheduler() + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + 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="" + ) + descript_editor + return (descript_editor,) + + +@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, + 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), + } + _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""" + ### TODO: explication `descript++` (variables et contraintes) + """) + 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: + + - **Add/modify configurations**: Try different tile sizes, loop orderings, or optimization combinations. The pre-built search space (variable `configurations` in function `explore` and lines above) may be poorly designed... + - **Change the acquisition function**: Add caching, error handling, or custom metrics + - **Modify the exploration loop**: Add early stopping or custom filtering + + The `explore()` function must `yield` tuples of `(index, total, config_name, performance)` for real-time progress display. + """) + 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: + i1+i1*j1<64 + """ + strategy = Strategy(graph, schedule_spec, partial_unrolls=True) + 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, + 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 = _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)", + ]) + + mo.md("\n".join(_summary_lines)) + 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() From 6261f0f22895020a6df2988a3d10991059e5f9b1 Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Thu, 28 May 2026 16:17:08 +0200 Subject: [PATCH 62/92] [DESCRIPT] d++ explication --- docs/tutorials/descript++_notebook.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py index fed3270e..bae9262c 100644 --- a/docs/tutorials/descript++_notebook.py +++ b/docs/tutorials/descript++_notebook.py @@ -449,7 +449,23 @@ def _(mo): @app.cell(hide_code=True) def _(mo): mo.md(r""" - ### TODO: explication `descript++` (variables et contraintes) + `descript` can also be used to describe loop structures without specifying every tile size. This allows you to easily write a set of strategies to explore. + + For example: + ```python + i: + j: + k: + j#16: + k#32: + i#i1: unroll + j#j1: vectorize + ``` + 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 @@ -539,10 +555,12 @@ def get_info(): i: j: k: + j#16: + k#32: unroll i#i1: unroll=i_u j#j1: vectorize=j_v constraints: - i1+i1*j1<64 + - j1+j1*i1<128 """ strategy = Strategy(graph, schedule_spec, partial_unrolls=True) backend = MLIR_Backend From d60911d461335371b1e3b8719d3d00ff78a872c4 Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Fri, 29 May 2026 15:35:18 +0200 Subject: [PATCH 63/92] [DESCRIPT] Cumulative perf graph [DESCRIPT] Edit marimo version for matplotlib support [DESCRIPT] matplotlib in deps --- docs/tutorials/descript++_notebook.py | 29 +++++++++++++++++++++++---- pyproject.toml | 3 ++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py index bae9262c..ed324211 100644 --- a/docs/tutorials/descript++_notebook.py +++ b/docs/tutorials/descript++_notebook.py @@ -22,6 +22,7 @@ def _(mo): 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.""" @@ -157,7 +158,7 @@ def run_exploration(generator, get_info=None): captured_info.update(get_info()) sorted_results = sorted(results, key=lambda x: x["perf"], reverse=True) - return sorted_results, captured_info + return sorted_results, captured_info, results def start_streaming_execution( *, @@ -251,6 +252,7 @@ def render(force: bool = False): execute_editor_code, get_backend_class, get_print_opts_dict, + plt, render_editor_output, run_exploration, start_streaming_execution, @@ -561,8 +563,9 @@ def get_info(): j#j1: vectorize=j_v constraints: - j1+j1*i1<128 + - i1*j1 > 16 """ - strategy = Strategy(graph, schedule_spec, partial_unrolls=True) + strategy = Strategy(graph, schedule_spec, partial_unrolls=False, partial_tiles=False) backend = MLIR_Backend configurations = list(strategy.sample(1000)) total = len(configurations) @@ -586,6 +589,7 @@ def _( explore_schedules_code, explore_schedules_intro, mo, + plt, run_exploration, run_explore_button, traceback, @@ -600,7 +604,7 @@ def _( except Exception: mo.stop(True, mo.md(f"**Code error:**\n```\n{traceback.format_exc()}\n```")) - _results, _captured_info = _namespace.get("results", ([], {})) + _results, _captured_info, _unsorted_results = _namespace.get("results", ([], {})) # Build results summary if not _results: @@ -633,7 +637,24 @@ def _( f"#### Best configuration: **{_best['sample']}** ({_best['perf']:.2f}% of peak)", ]) - mo.md("\n".join(_summary_lines)) + 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 diff --git a/pyproject.toml b/pyproject.toml index c74c0136..1231b437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,9 @@ dev = [ "pytest-xdist", "pyright==1.1.407", "types-PyYAML", - "marimo==0.19.6", + "marimo>=0.20.0", "ruff==0.14.10", + "matplotlib" ] [tool.setuptools] From 670e240ba337e6cdb52ffedbaabad777099a8552 Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Thu, 18 Jun 2026 15:37:37 +0200 Subject: [PATCH 64/92] [DESCRIPT] Edits to keep both parts coherent --- docs/tutorials/descript++_notebook.py | 36 +++++++++++++++------------ 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py index ed324211..002f7090 100644 --- a/docs/tutorials/descript++_notebook.py +++ b/docs/tutorials/descript++_notebook.py @@ -68,7 +68,7 @@ def get_output_options(backend_name: str) -> list: def create_backend_radio(label: str = "Backend:"): """Create a radio button for backend selection.""" - return mo.ui.radio(options=["MLIR", "TVM"], value="MLIR", label=label) + 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.""" @@ -340,6 +340,18 @@ def _(mo): @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 @@ -362,21 +374,13 @@ def matmul_graph(I: int, J: int, K: int, dtype: str) -> XTCGraph: graph = matmul_graph(I, J, K, dtype) backend = Backend(graph) - # Schedule specification - schedule_spec = """ - i: parallelize - k: - j: - i#8: unroll - j#16: vectorize - """ - # 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() @@ -395,8 +399,8 @@ def matmul_graph(I: int, J: int, K: int, dtype: str) -> XTCGraph: language="python", label="" ) - descript_editor - return (descript_editor,) + mo.vstack([descript_spec, descript_editor]) + return descript_editor, descript_spec @app.cell @@ -417,6 +421,7 @@ def _( descript_backend_radio, descript_editor, descript_output_radio, + descript_spec, execute_editor_code, get_backend_class, get_print_opts_dict, @@ -427,6 +432,7 @@ def _( "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) @@ -557,16 +563,14 @@ def get_info(): i: j: k: - j#16: - k#32: unroll i#i1: unroll=i_u j#j1: vectorize=j_v constraints: - j1+j1*i1<128 - - i1*j1 > 16 + - i1*j1 > 8 """ strategy = Strategy(graph, schedule_spec, partial_unrolls=False, partial_tiles=False) - backend = MLIR_Backend + backend = TVM_Backend configurations = list(strategy.sample(1000)) total = len(configurations) From 1c6833f973fc4cc5e6bb0f7075345723c1cd6620 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Wed, 17 Jun 2026 09:26:29 +0200 Subject: [PATCH 65/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction.py | 188 ++++++++++++++++++++- 1 file changed, 179 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py index 04fe3528..b8534eed 100644 --- a/docs/tutorials/hw_counters_introduction.py +++ b/docs/tutorials/hw_counters_introduction.py @@ -60,9 +60,9 @@ def _(mo): @app.cell def _(mo): # UI sliders - tile_i_ui = mo.ui.slider(start=16, stop=1024, step=16, value=16, label="Tile I (Rows)") - tile_j_ui = mo.ui.slider(start=16, stop=1024, step=16, value=16, label="Tile J (Cols)") - unroll_ui = mo.ui.slider(start=1, stop=32, step=1, value=2, label="Unroll factor") + tile_i_ui = mo.ui.slider(start=4, stop=256, step=4, value=4, label="Tile I (Rows)") + tile_j_ui = mo.ui.slider(start=16, stop=512, step=16, value=16, label="Tile J (Cols)") + unroll_ui = mo.ui.slider(start=1, stop=128, step=1, value=2, label="Unroll factor") return tile_i_ui, tile_j_ui, unroll_ui @@ -322,11 +322,12 @@ def _(btn_l1, exec_error, mo, module, platform): if tma_l1_counters and len(results_l1) > 0: if results_l1[0] < 0: - _l1_ui_display = mo.accordion({"Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```")}) + _l1_ui_display = mo.accordion({"Arch not supported or hardware counters not activated. Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```")}) else: _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] + _l1_data.sort(key=lambda x: x["Percentage (%)"], reverse=True) _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") - + try: import altair as alt @@ -341,7 +342,8 @@ def _(btn_l1, exec_error, mo, module, platform): ), legend=alt.Legend(title="Bottlenecks") ), - tooltip=["Category:N", "Percentage (%):Q"] + tooltip=["Category:N", "Percentage (%):Q"], + order=alt.Order("Percentage (%):Q", sort="descending"), ).properties(width=300, height=300, title="TMA L1 Breakdown") _l1_ui_display = mo.hstack([_l1_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) @@ -428,13 +430,13 @@ def _(btn_l2, exec_error, mo, module, platform): if tma_l2_counters and len(results_l2) > 0: if results_l2[0] < 0: _l2_ui_display = mo.vstack([ - mo.callout(mo.md("*Internal C resolver unsupported for L2. Showing `perf stat` output below.*"), kind="warn"), + mo.callout(mo.md("*Hardware counter not activated or internal C resolver unsupported for L2. Showing `perf stat` output below.*"), kind="warn"), mo.accordion({"Terminal Output from perf": mo.md(f"```text\n{error_l2}\n```")}) ]) else: _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] + _l2_data.sort(key=lambda x: x["Percentage (%)"], reverse=True) _l2_ui_table = mo.ui.table(_l2_data, label="Topdown L2 Results") - try: import altair as alt2 @@ -454,7 +456,8 @@ def _(btn_l2, exec_error, mo, module, platform): ), legend=alt2.Legend(title="Sub-Bottlenecks") ), - tooltip=["Sub-Category:N", "Percentage (%):Q"] + tooltip=["Sub-Category:N", "Percentage (%):Q"], + order=alt2.Order("Percentage (%):Q", sort="descending") ).properties(width=350, height=300, title="TMA L2 Breakdown") _l2_ui_display = mo.hstack([_l2_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) @@ -777,5 +780,172 @@ def _(btn_sandbox, mo, module, sandbox_editor): return +@app.cell(hide_code=True) +def _(mo): + btn_l3 = mo.ui.run_button(kind="neutral", label="Evaluate Topdown L3") + btn_asm = mo.ui.run_button(kind="neutral", label="Analyze Assembly (Spilling)") + + unroll_md = mo.md(f""" + --- + + ## 5. The "Register Pressure" Wall (Spilling) + + In optimization, "more" is not always better. Let's demonstrate **Register Pressure**. + + Modern CPUs have a limited number of physical vector registers (e.g., 16 for AVX2, 32 for AVX-512). These registers act as the "L0 Cache" and can hold a very small matrix block (e.g., 4x16). If your inner tile sizes (`Tile I`, `Tile J`) or your `Unroll` factor are too large, the compiler runs out of physical registers to store intermediate accumulations. It is forced to "spill" them to the stack (which lives in the L1 Cache). + + **The Experiment:** + 1. Set `Tile I` to **4**, `Tile J` to **32**, and `Unroll` to **2**. Click both buttons below. You should see a healthy Math/Memory ratio in the assembly. + 2. Now, set `Tile I` to **64** and `Tile J` to **64**. Click both buttons again. + + You should see the **`L1 Bound`** explode in the Topdown L3 chart, and a massive spike of memory operations (`vmovups`, `mov`, etc.) dominating the assembly instruction table! The CPU is drowning in L1 Cache transfers because the micro-tile no longer fits in its physical registers. + + If the tile is way too big, the bottleneck will be reported to **`L2 bound`** then **`L3 bound`** ... + + {mo.hstack([btn_l3, btn_asm])} + """) + + return btn_asm, btn_l3, unroll_md + + +@app.cell +def _(btn_l3, exec_error, mo, module, platform): + mo.stop(not btn_l3.value, mo.md("*Click 'Evaluate Topdown L3' to execute.*")) + + if module is None: + l3_ui = mo.md(f"**Compilation Error:**\n```python\n{exec_error}\n```") + else: + tma_l3_counters = ["TopdownL3"] if platform == "linux" else [] + evaluator_l3 = module.get_evaluator(validate=False, hw_counters=tma_l3_counters) + results_l3, code_l3, error_l3 = evaluator_l3.evaluate() + + _l3_labels = [ + "Branch Resteers", "Divider", "DRAM Bound", "DSB", "DSB Switches", "Few Uops", + "FP Arith", "Fused", "ICache Misses", "ITLB Misses", "L1 Bound", "L2 Bound", + "L3 Bound", "LCP", "Memory Ops", "Microcode Seq", "MITE", "MS Switches", + "Non-Fused Branches", "Other Light Ops", "Other Mispredicts", "Other Nukes", + "PMM Bound", "Ports Utilization", "Serializing", "Store Bound" + ] + + if tma_l3_counters and len(results_l3) > 0: + if results_l3[0] < 0: + l3_ui_display = mo.vstack([ + mo.md("*Native TopdownL3 unsupported. Showing fallback output.*"), + mo.accordion({"Fallback output": mo.md(f"```text\n{error_l3}\n```")}) + ]) + else: + _l3_data = [{"Metric": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l3_labels, results_l3) if v > 1.0] + + # move metrics < 1% into "others" + _other_sum = sum(v for v in results_l3 if v <= 1.0) + if _other_sum > 0: + _l3_data.append({"Metric": "Others (<1%)", "Percentage (%)": round(_other_sum, 2)}) + + _l3_data.sort(key=lambda x: x["Percentage (%)"], reverse=True) + _l3_ui_table = mo.ui.table(_l3_data, label="Topdown L3 (Metrics >1%)") + + try: + import altair as alt3 + color_condition = alt3.condition( + alt3.datum.Metric == 'L1 Bound', + alt3.value('#ef4444'), + alt3.Color('Metric:N') + ) + + _chart = alt3.Chart(alt3.Data(values=_l3_data)).mark_arc(innerRadius=40).encode( + theta=alt3.Theta(field="Percentage (%)", type="quantitative"), + color=color_condition, + order=alt3.Order("Percentage (%):Q", sort="descending"), + tooltip=["Metric:N", "Percentage (%):Q"] + ).properties(width=350, height=300, title="TMA L3 Breakdown") + + l3_ui_display = mo.hstack([_l3_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) + except ImportError: + l3_ui_display = _l3_ui_table + else: + l3_ui_display = mo.md("*TMA L3 is not supported on this machine.*") + + l3_ui = mo.vstack([mo.md(f"**Execution Code:** `{code_l3}`"), l3_ui_display]) + + return evaluator_l3, l3_ui, results_l3, tma_l3_counters + + +@app.cell +def _(mo,unroll_md): + unroll_md + return + +@app.cell +def _(mo,l3_ui): + l3_ui + return + +@app.cell +def _(btn_asm, mo, module): + mo.stop(not btn_asm.value, mo.md("*Click 'Analyze Assembly' to parse instructions.*")) + + so_path = getattr(module, "file_name", None) + if not so_path: + asm_ui = mo.md("**Module shared object (.so) not available for analysis.**") + else: + import subprocess + from collections import Counter + + try: + out = subprocess.check_output(["objdump", "-d", so_path], universal_newlines=True) + + counter = Counter() + mem_instr = 0 + math_instr = 0 + spill_count = 0 + + for line in out.splitlines(): + if ":" in line: + parts = line.split("\t") + if len(parts) >= 3: + instr_full = parts[2].strip() + mnemonic = instr_full.split()[0] + operands = instr_full[len(mnemonic):] + + if mnemonic.isalnum(): + counter[mnemonic] += 1 + + if "mov" in mnemonic or "ldr" in mnemonic or "str" in mnemonic: + mem_instr += 1 + # Spilling on stack + if "%rsp" in operands or "%rbp" in operands: + spill_count += 1 + print(f"[DEBUG] Spill at : {mnemonic} {operands}") + elif "add" in mnemonic or "mul" in mnemonic or "fma" in mnemonic: + math_instr += 1 + + if not counter: + asm_ui = mo.md("*Could not parse assembly instructions from the binary.*") + else: + asm_data = [{"Mnemonic": k, "Occurrences": v} for k, v in counter.most_common(20)] + ratio = mem_instr / max(math_instr, 1) + + alerts = [] + # Avoid the absolute value put at the end of the function + if spill_count > 1: + alerts.append(mo.callout(mo.md(f"**REGISTER SPILLING DETECTED!** Found **{spill_count}** memory operations hitting the stack (`%rsp` or `%rbp`). The compiler ran out of physical vector registers!"), kind="danger")) + elif ratio > 1.5: + alerts.append(mo.callout(mo.md(f"**High Memory Traffic ({ratio:.1f}x).** No stack spills detected, but the loop is drowning in memory operations. The compiler probably failed to keep the accumulator matrix 'C' in registers (missing Load/Store Hoisting)."), kind="warn")) + else: + alerts.append(mo.callout(mo.md(f"**Healthy Memory/Math Ratio ({ratio:.1f}x).** Accumulators are efficiently kept in registers."), kind="success")) + + asm_ui = mo.vstack(alerts + [mo.ui.table(asm_data, label="Top 20 Instructions")]) + + except Exception as e: + asm_ui = mo.md(f"*Failed to run objdump: {e}*") + + return asm_ui, + +@app.cell +def _(mo,asm_ui): + asm_ui + return + + if __name__ == "__main__": app.run() From 43165c68dc9fa6ef7765c2e51cd6c68f3e049b05 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 19 Jun 2026 09:08:33 +0200 Subject: [PATCH 66/92] [TMA] fix Comet Lake detection --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index cc5cf4a8..3046b340 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -72,6 +72,7 @@ intel_arch_t detect_intel_microarchitecture(void) { // Arch without TMA counter case 0x4E: case 0x5E: case 0x55: // Skylake-X / Cascade Lake-X / Cooper Lake case 0x8E: case 0x9E: // Whiskey Lake / Kaby Lake / Coffee Lake + case 0xA5: case 0xA6: // Comet Lake case 0x3D: case 0x47: case 0x4F: // Broadwell case 0x56: // Broadwell-DE return INTEL_SKYLAKE_CASCADE; @@ -79,7 +80,7 @@ intel_arch_t detect_intel_microarchitecture(void) { // Arch with TMA counter case 0x7E: case 0x7D: case 0x9D: // Ice Lake case 0x6A: case 0x6C: case 0x8C: // Ice Lake SP/D - case 0x8F: // Sapphire Rapids + case 0x8F: // Sapphire Rapids / Emerald Rapids case 0x97: case 0x9A: // Alder Lake case 0xB7: case 0xBA: case 0xBF: // Raptor Lake case 0xAA: case 0xAC: // Meteor Lake From 4de09919abcb1dfdb37ad95de97a4eac435dbb68 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 19 Jun 2026 14:50:03 +0200 Subject: [PATCH 67/92] [TMA]Fix broken runtime compilation --- src/xtc/runtimes/host/runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xtc/runtimes/host/runtime.py b/src/xtc/runtimes/host/runtime.py index 9b684e07..f6e2b70c 100644 --- a/src/xtc/runtimes/host/runtime.py +++ b/src/xtc/runtimes/host/runtime.py @@ -176,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 -O0 -g -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}" ) From 1db5a3525476e51e976dd858506f2a4430ce229e Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Mon, 22 Jun 2026 16:59:29 +0200 Subject: [PATCH 68/92] [DESCRIPT] Transformations --- docs/tutorials/descript++_notebook.py | 60 ++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py index 002f7090..f9bba070 100644 --- a/docs/tutorials/descript++_notebook.py +++ b/docs/tutorials/descript++_notebook.py @@ -1,6 +1,6 @@ import marimo -__generated_with = "0.23.8" +__generated_with = "0.20.0" app = marimo.App() @@ -662,6 +662,64 @@ def _( 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""" + ### 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, but this can be changed by adding `partial_unrolls=True` to 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. + + | 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""" From 0969cfbf6e315c2941865a6f6dc34bff7abf9176 Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Fri, 26 Jun 2026 12:29:03 +0200 Subject: [PATCH 69/92] [DESCRIPT] Update mypy python version To pass the issue with numpy --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1231b437..2e1497ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,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 From 0a5a474ee10d58b426293ce63ea8e35374c180e6 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Fri, 19 Jun 2026 16:35:33 +0200 Subject: [PATCH 70/92] [TMA]New version of the TMA tutorial --- docs/tutorials/hw_counters_introduction_v2.py | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 docs/tutorials/hw_counters_introduction_v2.py diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py new file mode 100644 index 00000000..eb311693 --- /dev/null +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -0,0 +1,340 @@ +""" +Schema architecture global (caches,front,back...) coloré en fonctions des métriques +Exemple de code de recuperation de compteur avec l'API XTC (une seule partie éditable?) +Tuilage imposé progressif avec le tuto (naif -> ... -> maxi tuilé opti) +Exemple de raté ? (4k aliasing) +Taille de tuile éditable -> possibilité de rentrer les chiffres +""" + + +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 notebook, 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 notebook will run, but TMA metrics might be unavailable or require `sudo` privileges."), 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 + ``` + """), kind="warn")) + + prerequisites_ui = mo.vstack(_warnings) + 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. + + --- + """) + return + + +@app.cell(hide_code=True) +def _(mo, prerequisites_ui): + mo.vstack([ + prerequisites_ui, + mo.accordion({ + "View Supported TMA Architectures": mo.md(""" + | Microarchitecture | Supported TMA Levels | Execution Mode | + |---|---|---| + | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3` | Native API | + | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2`, `TopdownL3` | Native API | + | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API | + | **ARM AArch64** | `TopdownL1 ?`, `TopdownL2 ?` | Native API | + | **Generic Linux (`perf`)** | `TopdownL1` to `TopdownL6` | System Fallback | + + *Unmapped architectures will automatically try to fallback to the `perf` tool using multiplexing.* + """) + }) + ]) + return + +@app.cell(hide_code=True) +def _(mo): + def plot_upipe(l1_res, l2_res=None, l3_res=None): + """Generates a hierarchical Left-to-Right block diagram for TMA.""" + if not l1_res or len(l1_res) < 4 or l1_res[0] < 0: + return mo.md("⚠️ **TMA Data not available.**") + + # Color palettes (L1, L2, L3) + c_ret = ("#86efac", "#4ade80", "#22c55e") + c_bad = ("#fca5a5", "#f87171", "#ef4444") + c_fe = ("#93c5fd", "#60a5fa", "#3b82f6") + c_be = ("#d8b4fe", "#c084fc", "#a855f7") + + desc_l1 = { + "Retiring": "Fraction of slots utilized by useful work.", + "Bad Spec": "Fraction of slots wasted due to incorrect speculations.", + "Frontend Bound": "Fraction of slots where the Frontend undersupplies the Backend.", + "Backend Bound": "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 (e.g., memory ordering issues).", + "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) or dependencies.", + "Mem Bound": "Execution stalled waiting for data from the memory (L1/L2/L3/RAM)." + } + desc_l3 = [ + "Stalls due to branch resteers at execution stage.", + "Cycles where the Divider unit was active.", + "Stalled on external memory (DRAM) accesses.", + "Limited by Decoded Stream Buffer (uop cache) pipeline.", + "Stalls due to switching from DSB to MITE pipelines.", + "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 Instruction TLB (ITLB) misses.", + "Stalled without missing the L1 Data (L1D) cache.", + "Stalled due to L2 cache accesses.", + "Stalled due to L3 cache accesses or sibling Core contention.", + "Stalls due to Length Changing Prefixes.", + "Memory load or store uops retired.", + "Uops fetched by the Microcode Sequencer (MS) unit.", + "Limited by MITE pipeline (legacy decode pipeline).", + "Stalls due to switching uop delivery to the MS unit.", + "Branch instructions that were not fused.", + "Remaining light uops not covered by other sibling nodes.", + "Stalls due to other misprediction cases (non-retired branches).", + "Machine Clears not related to memory ordering.", + "Stalled on accesses to Persistent Memory Modules (Optane).", + "Limited due to execution ports saturation (FPU/ALU contention).", + "Issue-pipeline stalled due to serializing operations.", + "Stalled due to store memory accesses and RFO requests." + ] + + def make_block(name, pct_global, color, desc="", level=1, children_html=""): + is_empty = pct_global < 0.1 + fill_pct = max(0.0, min(100.0, pct_global)) + + # CSS Linear gradient for vertical progress bar + bg_style = f"background: linear-gradient(to top, {color} {fill_pct}%, #f3f4f6 {fill_pct}%);" + text_color = "#9ca3af" if is_empty else "#000" + border_style = "1px dashed #d1d5db" if is_empty else "1px solid #d1d5db" + + safe_desc = desc.replace('"', '"') + tooltip = f"{name} ({pct_global:.1f}%)\n{safe_desc}" + + margin_b = "12px" if level == 1 else ("6px" if level == 2 else "3px") + + label_html = f''' +
+ {name} + {pct_global:.1f}% +
+ ''' + + if children_html: + return f''' +
+
+ {label_html} +
+
+ {children_html} +
+
+ ''' + else: + return f''' +
+ {label_html} +
+ ''' + + def get_l3(idx): + return l3_res[idx] if l3_res and len(l3_res) > idx and l3_res[idx] >= 0 else 0.0 + + def b_l3(name, idx, color): + return make_block(name, get_l3(idx), color, desc=desc_l3[idx], level=3) + + ret_html = bad_html = fe_html = be_html = "" + + if l2_res and len(l2_res) >= 8 and l2_res[0] >= 0: + l3_light_ops = l3_heavy_ops = l3_clears = l3_misp = l3_fetch_bw = l3_fetch_lat = l3_core_bnd = l3_mem_bnd = "" + + if l3_res and len(l3_res) >= 26 and l3_res[0] >= 0: + l3_light_ops = b_l3("FP Arith", 6, c_ret[2]) + b_l3("Mem Ops", 14, c_ret[2]) + b_l3("Fused", 7, c_ret[2]) + b_l3("Non-Fused Br", 18, c_ret[2]) + b_l3("Other Light", 19, c_ret[2]) + l3_heavy_ops = b_l3("Few Uops", 5, c_ret[2]) + b_l3("Microcode", 15, c_ret[2]) + l3_clears = b_l3("Other Nukes", 21, c_bad[2]) + l3_misp = b_l3("Branch Rest.", 0, c_bad[2]) + b_l3("Other Misp.", 20, c_bad[2]) + l3_fetch_bw = b_l3("MITE", 16, c_fe[2]) + b_l3("DSB", 3, c_fe[2]) + b_l3("DSB Swit.", 4, c_fe[2]) + b_l3("LCP", 13, c_fe[2]) + l3_fetch_lat = b_l3("ICache Miss", 8, c_fe[2]) + b_l3("ITLB Miss", 9, c_fe[2]) + b_l3("MS Switches", 17, c_fe[2]) + l3_core_bnd = b_l3("Divider", 1, c_be[2]) + b_l3("Ports Util.", 23, c_be[2]) + b_l3("Serializing", 24, c_be[2]) + l3_mem_bnd = b_l3("L1 Bound", 10, c_be[2]) + b_l3("L2 Bound", 11, c_be[2]) + b_l3("L3 Bound", 12, c_be[2]) + b_l3("DRAM Bound", 2, c_be[2]) + b_l3("Store Bound", 25, c_be[2]) + b_l3("PMM Bound", 22, c_be[2]) + + ret_html = make_block("Light Ops", l2_res[0], c_ret[1], desc_l2["Light Ops"], 2, l3_light_ops) + make_block("Heavy Ops", l2_res[1], c_ret[1], desc_l2["Heavy Ops"], 2, l3_heavy_ops) + bad_html = make_block("Clears", l2_res[2], c_bad[1], desc_l2["Clears"], 2, l3_clears) + make_block("Branch Misp", l2_res[3], c_bad[1], desc_l2["Branch Misp"], 2, l3_misp) + fe_html = make_block("Fetch BW", l2_res[4], c_fe[1], desc_l2["Fetch BW"], 2, l3_fetch_bw) + make_block("Fetch Lat", l2_res[5], c_fe[1], desc_l2["Fetch Lat"], 2, l3_fetch_lat) + be_html = make_block("Core Bound", l2_res[6], c_be[1], desc_l2["Core Bound"], 2, l3_core_bnd) + make_block("Mem Bound", l2_res[7], c_be[1], desc_l2["Mem Bound"], 2, l3_mem_bnd) + + l1_blocks = ( + make_block("Retiring", l1_res[0], c_ret[0], desc_l1["Retiring"], 1, ret_html) + + make_block("Bad Spec", l1_res[1], c_bad[0], desc_l1["Bad Spec"], 1, bad_html) + + make_block("Frontend Bound", l1_res[2], c_fe[0], desc_l1["Frontend Bound"], 1, fe_html) + + make_block("Backend Bound", l1_res[3], c_be[0], desc_l1["Backend Bound"], 1, be_html) + ) + + return mo.Html(f""" +
+ {l1_blocks} +
+ """) + + return plot_upipe, + +################################################################################################## +##########################################EX 1############################################### +################################################################################################## + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## Step 1: The Naive Implementation + Let's evaluate a standard, unoptimized Matrix Multiplication. No tiling, no vectorization, no unrolling. + + ``` + for i in 128 + for j in 512 + for k in 1024 + ``` + + """) + return + + + +@app.cell +def _(mo, platform): + + import xtc.graphs.xtc.op as O + from xtc.backends.mlir import Backend + from xtc.schedules.descript import descript_scheduler + + I, J, K, dtype = 128, 512, 1024, "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) + + # Basic Schedule based on UI inputs + schedule_spec = ''' + i: + j: + k: + ''' + + + scheduler = backend.get_scheduler() + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + spec=schedule_spec + ) + + sched = scheduler.schedule() + + compiler = backend.get_compiler( + dump_file="matmul_mlir", + print_source_ir=False, + print_transformed_ir=False, + print_assembly=False, + shared_lib=True + ) + + module = compiler.compile(sched) + + + # Evaluation + tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] + evaluator = module.get_evaluator(validate=False, hw_counters=tma_counters) + res, code, err = evaluator.evaluate() + + # Slicing results + ex1_l1_res = res[0:4] if len(res) >= 4 else None + ex1_l2_res = res[4:12] if len(res) >= 12 else None + ex1_l3_res = res[12:38] if len(res) >= 38 else None + + print(f"[DEBUG] ex1 l1 : {ex1_l1_res}") + print(f"[DEBUG] ex1 l2 : {ex1_l2_res}") + print(f"[DEBUG] ex1 l3 : {ex1_l3_res}") + + + return I, J, K, a, b, backend, code, dtype, err, evaluator, gb, ex1_l1_res, ex1_l2_res, ex1_l3_res,module, res, schedule_spec, scheduler, tma_counters + + +@app.cell +def _(err, ex1_l1_res, ex1_l2_res, ex1_l3_res, mo, plot_upipe): + # Display the upipe diagram + if err: + ex1_output = mo.md(f"**Error:**\n```text\n{err}\n```") + else: + ex1_output = mo.vstack([ + mo.md("**Microarchitecture Pipeline Bottlenecks:**"), + plot_upipe(ex1_l1_res, ex1_l2_res, ex1_l3_res) + ]) + + ex1_output + return ex1_output, + + + +################################################################################################## +##########################################EX 2############################################### +################################################################################################## + +# todo : Exemples de plus en plus avancé d'optimisation avec visuel de compteurs + + + +if __name__ == "__main__": + app.run() + + + + + + From 4a478663d7728a7c128093c5037de7fbbf3b0a76 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 22 Jun 2026 11:17:11 +0200 Subject: [PATCH 71/92] [TMA]add plotly as requirement --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 0b888e5b..9a085a18 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -12,4 +12,4 @@ pyright==1.1.407 types-PyYAML ruff==0.14.10 altair - +plotly From 9af923d62904b9560f70083e3ae40c4269357a06 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 22 Jun 2026 11:17:58 +0200 Subject: [PATCH 72/92] [TMA] Detection for all Intel models --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 39 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 3046b340..3525a72c 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -67,23 +67,48 @@ intel_arch_t detect_intel_microarchitecture(void) { int family, model; get_cpu_family_model(&family, &model); + // source : https://github.com/torvalds/linux/blob/ma ster/arch/x86/include/asm/intel-family.h + if (family == 6) { switch (model) { // Arch without TMA counter case 0x4E: case 0x5E: case 0x55: // Skylake-X / Cascade Lake-X / Cooper Lake - case 0x8E: case 0x9E: // Whiskey Lake / Kaby Lake / Coffee Lake + case 0x8E: case 0x9E: // Whiskey Lake / Kaby Lake / Coffee Lake / Amber lake case 0xA5: case 0xA6: // Comet Lake case 0x3D: case 0x47: case 0x4F: // Broadwell case 0x56: // Broadwell-DE + case 0x66: // Cannon Lake return INTEL_SKYLAKE_CASCADE; // Arch with TMA counter - case 0x7E: case 0x7D: case 0x9D: // Ice Lake - case 0x6A: case 0x6C: case 0x8C: // Ice Lake SP/D - case 0x8F: // Sapphire Rapids / Emerald Rapids - case 0x97: case 0x9A: // Alder Lake - case 0xB7: case 0xBA: case 0xBF: // Raptor Lake - case 0xAA: case 0xAC: // Meteor Lake + case 0x7E: case 0x7D: case 0x9D: // Ice Lake Client + case 0x6A: case 0x6C: // Ice Lake Server (Xeon) + case 0xA7: // Rocket Lake (Cypress Cove) + case 0x8C: case 0x8D: // Tiger Lake + case 0x8A: // Lakefield + case 0x8F: // Sapphire Rapids (Xeon) + case 0xCF: // Emerald Rapids (Xeon) + case 0xAD: case 0xAE: // Granite Rapids (Xeon X et D) + case 0x97: case 0x9A: // Alder Lake (12th Gen) + case 0xB7: case 0xBA: case 0xBF: // Raptor Lake (13th/14th Gen) + case 0xD7: // Bartlett Lake + case 0xAA: case 0xAC: // Meteor Lake (Core Ultra 1) + case 0xB5: case 0xC5: case 0xC6: // Arrow Lake + case 0xBD: // Lunar Lake (Core Ultra 2) + case 0xCC: case 0xE5: // Panther Lake + case 0xD5: // Wildcat Lake + return INTEL_ICELAKE_SAPPHIRE; + } + } + else if (family == 18) { + switch (model) { + case 0x01: case 0x03: // Nova Lake + return INTEL_ICELAKE_SAPPHIRE; + } + } + else if (family == 19) { + switch (model) { + case 0x01: // Diamond Rapids (Xeon) return INTEL_ICELAKE_SAPPHIRE; } } From 2471e756280409733928f032bccf2b4c6488f7f6 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 22 Jun 2026 11:18:56 +0200 Subject: [PATCH 73/92] [TMA]update HW counters Marimo tutorial --- docs/tutorials/hw_counters_introduction_v2.py | 732 ++++++++++++------ 1 file changed, 503 insertions(+), 229 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index eb311693..d80875c1 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -45,6 +45,8 @@ def _(mo, platform): 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) @@ -62,273 +64,546 @@ def _(mo): """) return - -@app.cell(hide_code=True) -def _(mo, prerequisites_ui): - mo.vstack([ - prerequisites_ui, - mo.accordion({ - "View Supported TMA Architectures": mo.md(""" - | Microarchitecture | Supported TMA Levels | Execution Mode | - |---|---|---| - | **Intel Skylake / Cascade Lake** | `TopdownL1`, `TopdownL2`, `TopdownL3` | Native API | - | **Intel Modern (Ice Lake+)** | `TopdownL1`, `TopdownL2`, `TopdownL3` | Native API | - | **AMD Zen 4** | `TopdownL1`, `TopdownL2` | Native API | - | **ARM AArch64** | `TopdownL1 ?`, `TopdownL2 ?` | Native API | - | **Generic Linux (`perf`)** | `TopdownL1` to `TopdownL6` | System Fallback | - - *Unmapped architectures will automatically try to fallback to the `perf` tool using multiplexing.* - """) - }) - ]) - 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 Left-to-Right block diagram for TMA.""" + """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.**") - # Color palettes (L1, L2, L3) - c_ret = ("#86efac", "#4ade80", "#22c55e") - c_bad = ("#fca5a5", "#f87171", "#ef4444") - c_fe = ("#93c5fd", "#60a5fa", "#3b82f6") - c_be = ("#d8b4fe", "#c084fc", "#a855f7") - - desc_l1 = { - "Retiring": "Fraction of slots utilized by useful work.", - "Bad Spec": "Fraction of slots wasted due to incorrect speculations.", - "Frontend Bound": "Fraction of slots where the Frontend undersupplies the Backend.", - "Backend Bound": "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 (e.g., memory ordering issues).", - "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) or dependencies.", - "Mem Bound": "Execution stalled waiting for data from the memory (L1/L2/L3/RAM)." - } - desc_l3 = [ - "Stalls due to branch resteers at execution stage.", - "Cycles where the Divider unit was active.", - "Stalled on external memory (DRAM) accesses.", - "Limited by Decoded Stream Buffer (uop cache) pipeline.", - "Stalls due to switching from DSB to MITE pipelines.", - "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 Instruction TLB (ITLB) misses.", - "Stalled without missing the L1 Data (L1D) cache.", - "Stalled due to L2 cache accesses.", - "Stalled due to L3 cache accesses or sibling Core contention.", - "Stalls due to Length Changing Prefixes.", - "Memory load or store uops retired.", - "Uops fetched by the Microcode Sequencer (MS) unit.", - "Limited by MITE pipeline (legacy decode pipeline).", - "Stalls due to switching uop delivery to the MS unit.", - "Branch instructions that were not fused.", - "Remaining light uops not covered by other sibling nodes.", - "Stalls due to other misprediction cases (non-retired branches).", - "Machine Clears not related to memory ordering.", - "Stalled on accesses to Persistent Memory Modules (Optane).", - "Limited due to execution ports saturation (FPU/ALU contention).", - "Issue-pipeline stalled due to serializing operations.", - "Stalled due to store memory accesses and RFO requests." - ] - - def make_block(name, pct_global, color, desc="", level=1, children_html=""): - is_empty = pct_global < 0.1 - fill_pct = max(0.0, min(100.0, pct_global)) - - # CSS Linear gradient for vertical progress bar - bg_style = f"background: linear-gradient(to top, {color} {fill_pct}%, #f3f4f6 {fill_pct}%);" - text_color = "#9ca3af" if is_empty else "#000" - border_style = "1px dashed #d1d5db" if is_empty else "1px solid #d1d5db" - - safe_desc = desc.replace('"', '"') - tooltip = f"{name} ({pct_global:.1f}%)\n{safe_desc}" - - margin_b = "12px" if level == 1 else ("6px" if level == 2 else "3px") - - label_html = f''' -
- {name} - {pct_global:.1f}% -
- ''' - - if children_html: - return f''' -
-
- {label_html} -
-
- {children_html} -
-
- ''' - else: - return f''' -
- {label_html} -
- ''' - - def get_l3(idx): - return l3_res[idx] if l3_res and len(l3_res) > idx and l3_res[idx] >= 0 else 0.0 - - def b_l3(name, idx, color): - return make_block(name, get_l3(idx), color, desc=desc_l3[idx], level=3) - - ret_html = bad_html = fe_html = be_html = "" - - if l2_res and len(l2_res) >= 8 and l2_res[0] >= 0: - l3_light_ops = l3_heavy_ops = l3_clears = l3_misp = l3_fetch_bw = l3_fetch_lat = l3_core_bnd = l3_mem_bnd = "" - - if l3_res and len(l3_res) >= 26 and l3_res[0] >= 0: - l3_light_ops = b_l3("FP Arith", 6, c_ret[2]) + b_l3("Mem Ops", 14, c_ret[2]) + b_l3("Fused", 7, c_ret[2]) + b_l3("Non-Fused Br", 18, c_ret[2]) + b_l3("Other Light", 19, c_ret[2]) - l3_heavy_ops = b_l3("Few Uops", 5, c_ret[2]) + b_l3("Microcode", 15, c_ret[2]) - l3_clears = b_l3("Other Nukes", 21, c_bad[2]) - l3_misp = b_l3("Branch Rest.", 0, c_bad[2]) + b_l3("Other Misp.", 20, c_bad[2]) - l3_fetch_bw = b_l3("MITE", 16, c_fe[2]) + b_l3("DSB", 3, c_fe[2]) + b_l3("DSB Swit.", 4, c_fe[2]) + b_l3("LCP", 13, c_fe[2]) - l3_fetch_lat = b_l3("ICache Miss", 8, c_fe[2]) + b_l3("ITLB Miss", 9, c_fe[2]) + b_l3("MS Switches", 17, c_fe[2]) - l3_core_bnd = b_l3("Divider", 1, c_be[2]) + b_l3("Ports Util.", 23, c_be[2]) + b_l3("Serializing", 24, c_be[2]) - l3_mem_bnd = b_l3("L1 Bound", 10, c_be[2]) + b_l3("L2 Bound", 11, c_be[2]) + b_l3("L3 Bound", 12, c_be[2]) + b_l3("DRAM Bound", 2, c_be[2]) + b_l3("Store Bound", 25, c_be[2]) + b_l3("PMM Bound", 22, c_be[2]) - - ret_html = make_block("Light Ops", l2_res[0], c_ret[1], desc_l2["Light Ops"], 2, l3_light_ops) + make_block("Heavy Ops", l2_res[1], c_ret[1], desc_l2["Heavy Ops"], 2, l3_heavy_ops) - bad_html = make_block("Clears", l2_res[2], c_bad[1], desc_l2["Clears"], 2, l3_clears) + make_block("Branch Misp", l2_res[3], c_bad[1], desc_l2["Branch Misp"], 2, l3_misp) - fe_html = make_block("Fetch BW", l2_res[4], c_fe[1], desc_l2["Fetch BW"], 2, l3_fetch_bw) + make_block("Fetch Lat", l2_res[5], c_fe[1], desc_l2["Fetch Lat"], 2, l3_fetch_lat) - be_html = make_block("Core Bound", l2_res[6], c_be[1], desc_l2["Core Bound"], 2, l3_core_bnd) + make_block("Mem Bound", l2_res[7], c_be[1], desc_l2["Mem Bound"], 2, l3_mem_bnd) - - l1_blocks = ( - make_block("Retiring", l1_res[0], c_ret[0], desc_l1["Retiring"], 1, ret_html) + - make_block("Bad Spec", l1_res[1], c_bad[0], desc_l1["Bad Spec"], 1, bad_html) + - make_block("Frontend Bound", l1_res[2], c_fe[0], desc_l1["Frontend Bound"], 1, fe_html) + - make_block("Backend Bound", l1_res[3], c_be[0], desc_l1["Backend Bound"], 1, be_html) - ) - - return mo.Html(f""" -
- {l1_blocks} -
- """) + 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, -################################################################################################## -##########################################EX 1############################################### -################################################################################################## + +#@app.cell(hide_code=True) +#def _(mo): +# mo.md(r""" +# --- +# ## Step 1: The Naive Implementation +# Let's evaluate a standard, unoptimized Matrix Multiplication. No tiling, no vectorization, no unrolling. +# +# ``` +# for i in 128 +# for j in 512 +# for k in 1024 +# ``` +# +# """) +# return +# +# +# +#@app.cell +#def _(mo, platform): +# +# import xtc.graphs.xtc.op as O +# from xtc.backends.mlir import Backend +# from xtc.schedules.descript import descript_scheduler +# +# I, J, K, dtype = 128, 512, 1024, "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) +# +# # Basic Schedule based on UI inputs +# schedule_spec = ''' +# i: +# j: +# k: +# ''' +# +# +# scheduler = backend.get_scheduler() +# descript_scheduler( +# scheduler=scheduler, +# node_name="C", +# abstract_dims=["i", "j", "k"], +# spec=schedule_spec +# ) +# +# sched = scheduler.schedule() +# +# compiler = backend.get_compiler( +# dump_file="matmul_mlir", +# print_source_ir=False, +# print_transformed_ir=False, +# print_assembly=False, +# shared_lib=True +# ) +# +# module = compiler.compile(sched) +# +# +# # Evaluation +# tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] +# evaluator = module.get_evaluator(validate=False, hw_counters=tma_counters) +# res, code, err = evaluator.evaluate() +# +# # Slicing results +# ex1_l1_res = res[0:4] if len(res) >= 4 else None +# ex1_l2_res = res[4:12] if len(res) >= 12 else None +# ex1_l3_res = res[12:38] if len(res) >= 38 else None +# +# print(f"[DEBUG] ex1 l1 : {ex1_l1_res}") +# print(f"[DEBUG] ex1 l2 : {ex1_l2_res}") +# print(f"[DEBUG] ex1 l3 : {ex1_l3_res}") +# +# +# return I, J, K, a, b, backend, code, dtype, err, evaluator, gb, ex1_l1_res, ex1_l2_res, ex1_l3_res,module, res, schedule_spec, scheduler, tma_counters +# +# +#@app.cell +#def _(err, ex1_l1_res, ex1_l2_res, ex1_l3_res, mo, plot_upipe): +# # Display the upipe diagram +# if err: +# ex1_output = mo.md(f"**Error:**\n```text\n{err}\n```") +# else: +# ex1_output = mo.vstack([ +# mo.md("**Microarchitecture Pipeline Bottlenecks:**"), +# plot_upipe(ex1_l1_res, ex1_l2_res, ex1_l3_res) +# ]) +# +# ex1_output +# return ex1_output, + + + + + +@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 + + # 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"], spec=schedule_spec) + + compiler = backend.get_compiler(dump_file="matmul_mlir", shared_lib=True) + module = compiler.compile(scheduler.schedule()) + + 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 + + return mo.vstack([ + plot_upipe(l1_res, l2_res, l3_res), + mo.accordion({"Click here to see the XTC schedule": mo.md(f"```\n{schedule_spec}\n```")}) + ]) + return run_experiment, + @app.cell(hide_code=True) def _(mo): - mo.md(r""" + btn_ex1 = mo.ui.run_button(label="Run Step 1", kind="neutral") + + mo.md(f""" --- - ## Step 1: The Naive Implementation - Let's evaluate a standard, unoptimized Matrix Multiplication. No tiling, no vectorization, no unrolling. - - ``` - for i in 128 - for j in 512 - for k in 1024 + ## 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 + return btn_ex1, + +@app.cell(hide_code=True) +def _(btn_ex1, mo, run_experiment): + mo.stop(not btn_ex1.value) + spec_1 = ''' +i: +j: +k: +''' + run_experiment(spec_1) + return spec_1, -@app.cell -def _(mo, platform): - import xtc.graphs.xtc.op as O - from xtc.backends.mlir import Backend - from xtc.schedules.descript import descript_scheduler +@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). + + ```python + for i in 512: + for j_out in 32: + for k in 512: + for j_vec in 16: # Vectorized execution (e.g., AVX-512) + C[i, ...] += A[i, k] * B[k, ...] + ``` + *Expectation: Higher Retiring, but still bottlenecked by memory loads.* + + {btn_ex2} + """) + return btn_ex2, - I, J, K, dtype = 128, 512, 1024, "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") +@app.cell(hide_code=True) +def _(btn_ex2, mo, run_experiment): + mo.stop(not btn_ex2.value) + + spec_2 = ''' +i: +j: +k: +j#16: vectorize +''' + run_experiment(spec_2) + return spec_2, - backend = Backend(gb.graph) - # Basic Schedule based on UI inputs - schedule_spec = ''' - i: - j: - k: - ''' +@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, hiding instruction latency by keeping accumulators inside physical registers. + + ```python + for i_out in 256: + for j_out in 32: + for k in 512: + for i_unroll in 2: # Unrolled + for j_vec in 16: # Vectorized + C[...] += A[...] * B[...] + ``` + *Expectation: Retiring improves, but the CPU still re-loads the accumulator `C` at every `k` iteration.* + + {btn_ex3} + """) + return btn_ex3, - scheduler = backend.get_scheduler() - descript_scheduler( - scheduler=scheduler, - node_name="C", - abstract_dims=["i", "j", "k"], - spec=schedule_spec - ) +@app.cell(hide_code=True) +def _(btn_ex3, mo, run_experiment): + mo.stop(not btn_ex3.value) + + spec_3 = ''' +i: +j: +k: +i#2: unroll +j#16: vectorize +''' + run_experiment(spec_3) + return spec_3, - sched = scheduler.schedule() - compiler = backend.get_compiler( - dump_file="matmul_mlir", - print_source_ir=False, - print_transformed_ir=False, - print_assembly=False, - shared_lib=True - ) +@app.cell(hide_code=True) +def _(mo): + btn_ex4 = mo.ui.run_button(label="Run Step 4", kind="neutral") - module = compiler.compile(sched) - + 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 + for i_out in 256: + for j_out in 32: + # Load C into registers once + for k in 512: + for i_unroll in 2: + for j_vec in 16: + C_reg[...] += A[...] * B[...] + # Store C to memory once + ``` + *Expectation: Massive drop in L1 Bound. The CPU becomes highly Core Bound (saturating ALUs).* - # Evaluation - tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] - evaluator = module.get_evaluator(validate=False, hw_counters=tma_counters) - res, code, err = evaluator.evaluate() - - # Slicing results - ex1_l1_res = res[0:4] if len(res) >= 4 else None - ex1_l2_res = res[4:12] if len(res) >= 12 else None - ex1_l3_res = res[12:38] if len(res) >= 38 else None - - print(f"[DEBUG] ex1 l1 : {ex1_l1_res}") - print(f"[DEBUG] ex1 l2 : {ex1_l2_res}") - print(f"[DEBUG] ex1 l3 : {ex1_l3_res}") + {btn_ex4} + """) + return btn_ex4, - return I, J, K, a, b, backend, code, dtype, err, evaluator, gb, ex1_l1_res, ex1_l2_res, ex1_l3_res,module, res, schedule_spec, scheduler, tma_counters +@app.cell(hide_code=True) +def _(btn_ex4, mo, run_experiment): + mo.stop(not btn_ex4.value) + + spec_4 = ''' + i: + j: + i#2: unroll + j#16: vectorize + k: vectorize + ''' + run_experiment(spec_4) + return spec_4, -@app.cell -def _(err, ex1_l1_res, ex1_l2_res, ex1_l3_res, mo, plot_upipe): - # Display the upipe diagram - if err: - ex1_output = mo.md(f"**Error:**\n```text\n{err}\n```") - else: - ex1_output = mo.vstack([ - mo.md("**Microarchitecture Pipeline Bottlenecks:**"), - plot_upipe(ex1_l1_res, ex1_l2_res, ex1_l3_res) - ]) +@app.cell(hide_code=True) +def _(mo): + btn_ex5 = mo.ui.run_button(label="Run Step 5", kind="neutral") - ex1_output - return ex1_output, + 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 + for i_blk in 64, for j_blk in 64, for k_blk in 64: # Cache blocking + for i_out, for j_out: + for k_in in 64: + for i_unroll in 2, for j_vec in 16: + C_reg[...] += A[...] * B[...] + ``` + *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, mo, run_experiment): + mo.stop(not btn_ex5.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, -################################################################################################## -##########################################EX 2############################################### -################################################################################################## -# todo : Exemples de plus en plus avancé d'optimisation avec visuel de compteurs +@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 + for i_blk in 64, for j_blk in 64, for k_blk in 64: + for i_out, for j_out: + for k_in in 64: + for i_unroll in 3, for j_vec in 16: # Optimized Register Tile + C_reg[...] += A[...] * B[...] + ``` + *Expectation: Maximum Retiring percentage. Peak theoretical performance.* + + {btn_ex6} + """) + return btn_ex6, +@app.cell(hide_code=True) +def _(btn_ex6, mo, run_experiment): + mo.stop(not btn_ex6.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, if __name__ == "__main__": app.run() @@ -336,5 +611,4 @@ def _(err, ex1_l1_res, ex1_l2_res, ex1_l3_res, mo, plot_upipe): - - +# TODO : Rajouter le calcul du peek-perf a chaque exp From 622e026847472167504d172f647584f27465acc9 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 23 Jun 2026 06:12:27 +0200 Subject: [PATCH 74/92] [TMA]run all button --- docs/tutorials/hw_counters_introduction_v2.py | 114 ++++++++++-------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index d80875c1..3f23049f 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -1,5 +1,5 @@ """ -Schema architecture global (caches,front,back...) coloré en fonctions des métriques +Schema architecture global (caches,front,back...) coloré en fonctions des métriques Exemple de code de recuperation de compteur avec l'API XTC (une seule partie éditable?) Tuilage imposé progressif avec le tuto (naif -> ... -> maxi tuilé opti) Exemple de raté ? (4k aliasing) @@ -34,7 +34,7 @@ def _(mo): @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 notebook will run, but TMA metrics might be unavailable or require `sudo` privileges."), kind="danger")) @@ -64,7 +64,7 @@ def _(mo): """) return -# µpipe Sankey diagram +# µpipe Sankey diagram @app.cell(hide_code=True) def _(mo): def plot_upipe(l1_res, l2_res=None, l3_res=None): @@ -175,7 +175,7 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): 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)] @@ -231,7 +231,25 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): 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): @@ -249,7 +267,7 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): # """) # return # -# +# # #@app.cell #def _(mo, platform): @@ -269,7 +287,7 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): # # # Basic Schedule based on UI inputs # schedule_spec = ''' -# i: +# i: # j: # k: # ''' @@ -277,9 +295,9 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): # # scheduler = backend.get_scheduler() # descript_scheduler( -# scheduler=scheduler, -# node_name="C", -# abstract_dims=["i", "j", "k"], +# scheduler=scheduler, +# node_name="C", +# abstract_dims=["i", "j", "k"], # spec=schedule_spec # ) # @@ -292,10 +310,10 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): # print_assembly=False, # shared_lib=True # ) -# +# # module = compiler.compile(sched) # -# +# # # Evaluation # tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] # evaluator = module.get_evaluator(validate=False, hw_counters=tma_counters) @@ -324,7 +342,7 @@ def process_l2(l2_name, l2_idx, parent_node, color, desc, l3_mappings=None): # mo.md("**Microarchitecture Pipeline Bottlenecks:**"), # plot_upipe(ex1_l1_res, ex1_l2_res, ex1_l3_res) # ]) -# +# # ex1_output # return ex1_output, @@ -349,9 +367,9 @@ def run_experiment(schedule_spec): backend = Backend(gb.graph) scheduler = backend.get_scheduler() - + descript_scheduler(scheduler=scheduler, node_name="C", abstract_dims=["i", "j", "k"], spec=schedule_spec) - + compiler = backend.get_compiler(dump_file="matmul_mlir", shared_lib=True) module = compiler.compile(scheduler.schedule()) @@ -375,7 +393,7 @@ def run_experiment(schedule_spec): @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 @@ -388,16 +406,16 @@ def _(mo): 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, mo, run_experiment): - mo.stop(not btn_ex1.value) - +def _(btn_ex1, btn_all, mo, run_experiment): + mo.stop(not (btn_ex1.value or btn_all.value)) + spec_1 = ''' i: j: @@ -410,7 +428,7 @@ def _(btn_ex1, mo, run_experiment): @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 @@ -424,16 +442,16 @@ def _(mo): C[i, ...] += A[i, k] * B[k, ...] ``` *Expectation: Higher Retiring, but still bottlenecked by memory loads.* - + {btn_ex2} """) return btn_ex2, @app.cell(hide_code=True) -def _(btn_ex2, mo, run_experiment): - mo.stop(not btn_ex2.value) - +def _(btn_ex2, btn_all, mo, run_experiment): + mo.stop(not (btn_ex2.value or btn_all.value)) + spec_2 = ''' i: j: @@ -447,7 +465,7 @@ def _(btn_ex2, mo, run_experiment): @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) @@ -462,16 +480,16 @@ def _(mo): C[...] += A[...] * B[...] ``` *Expectation: Retiring improves, 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, mo, run_experiment): - mo.stop(not btn_ex3.value) - +def _(btn_ex3, btn_all, mo, run_experiment): + mo.stop(not (btn_ex3.value or btn_all.value)) + spec_3 = ''' i: j: @@ -486,7 +504,7 @@ def _(btn_ex3, mo, run_experiment): @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 @@ -496,23 +514,23 @@ def _(mo): for i_out in 256: for j_out in 32: # Load C into registers once - for k in 512: + for k in 512: for i_unroll in 2: for j_vec in 16: C_reg[...] += A[...] * B[...] # Store C to memory once ``` *Expectation: Massive drop in L1 Bound. The CPU becomes highly Core Bound (saturating ALUs).* - + {btn_ex4} """) return btn_ex4, @app.cell(hide_code=True) -def _(btn_ex4, mo, run_experiment): - mo.stop(not btn_ex4.value) - +def _(btn_ex4, btn_all, mo, run_experiment): + mo.stop(not (btn_ex4.value or btn_all.value)) + spec_4 = ''' i: j: @@ -527,7 +545,7 @@ def _(btn_ex4, mo, run_experiment): @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 @@ -536,21 +554,21 @@ def _(mo): ```python for i_blk in 64, for j_blk in 64, for k_blk in 64: # Cache blocking for i_out, for j_out: - for k_in in 64: + for k_in in 64: for i_unroll in 2, for j_vec in 16: C_reg[...] += A[...] * B[...] ``` *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, mo, run_experiment): - mo.stop(not btn_ex5.value) - +def _(btn_ex5, btn_all, mo, run_experiment): + mo.stop(not (btn_ex5.value or btn_all.value)) + spec_5 = ''' i: j: @@ -568,7 +586,7 @@ def _(btn_ex5, mo, run_experiment): @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) @@ -577,21 +595,21 @@ def _(mo): ```python for i_blk in 64, for j_blk in 64, for k_blk in 64: for i_out, for j_out: - for k_in in 64: + for k_in in 64: for i_unroll in 3, for j_vec in 16: # Optimized Register Tile C_reg[...] += A[...] * B[...] ``` *Expectation: Maximum Retiring percentage. Peak theoretical performance.* - + {btn_ex6} """) return btn_ex6, @app.cell(hide_code=True) -def _(btn_ex6, mo, run_experiment): - mo.stop(not btn_ex6.value) - +def _(btn_ex6, btn_all, mo, run_experiment): + mo.stop(not (btn_ex6.value or btn_all.value)) + spec_6 = ''' i: j: From 0b5035f51127ca70f0636978634f0eca6d44f30a Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 24 Jun 2026 06:01:04 +0200 Subject: [PATCH 75/92] [TMA]fix pseudo-code --- docs/tutorials/hw_counters_introduction_v2.py | 153 ++++++++++++++---- 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index 3f23049f..1fb7918c 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -433,15 +433,19 @@ def _(mo): --- ## 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 512: - for j_out in 32: - for k in 512: - for j_vec in 16: # Vectorized execution (e.g., AVX-512) - C[i, ...] += A[i, k] * B[k, ...] + 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 bottlenecked by memory loads.* + + *Expectation: Higher Retiring, but still heavily bottlenecked by memory loads since `C` is read and written 512 times for nothing.* {btn_ex2} """) @@ -469,17 +473,20 @@ def _(mo): mo.md(f""" --- ## Step 3: Register Tiling (2x16) - Unrolling the outer loop allows the CPU to process multiple vectors simultaneously, hiding instruction latency by keeping accumulators inside physical registers. + 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_out in 256: - for j_out in 32: - for k in 512: - for i_unroll in 2: # Unrolled - for j_vec in 16: # Vectorized - C[...] += A[...] * B[...] + 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, but the CPU still re-loads the accumulator `C` at every `k` iteration.* + + *Expectation: Retiring improves (Instruction-Level Parallelism), but the CPU still re-loads the accumulator `C` at every `k` iteration.* {btn_ex3} """) @@ -511,16 +518,35 @@ def _(mo): 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 - for i_out in 256: - for j_out in 32: - # Load C into registers once - for k in 512: - for i_unroll in 2: - for j_vec in 16: - C_reg[...] += A[...] * B[...] - # Store C to memory once + I = 512 + J = 512 + K = 512 + # 1. Global navigation (RAM) + for i_out in range(I / 2): # Steps of 2 + for j_out in range(J / 16): # Steps of 16 + + # --- LOAD HOISTING --- + # We load 2 independent vector registers (16 floats each) once! + C_vec_0 = C[i, j : j+16] + C_vec_1 = C[i + 1, j : j+16] + + # 2. Accumulation loop + for k in range(K): # 4096 iterations! + + # Vector Load (16 floats loaded at once) + B_vec = B[k, j : j+16] + + # 3. Unrolled & Vectorized Micro-kernel + C_vec_0 += A[i, k] * B_vec + C_vec_1 += A[i + 1, k] * B_vec + + # --- STORE HOISTING --- + # We trigger 2 simultaneous vector stores to memory! + C[i, j : j+16] = C_vec_0 + C[i + 1, j : j+16] = C_vec_1 ``` - *Expectation: Massive drop in L1 Bound. The CPU becomes highly Core Bound (saturating ALUs).* + + *Expectation: Massive drop in L1 Bound since C is no longer repeatedly spilled to the cache.* {btn_ex4} """) @@ -551,13 +577,41 @@ def _(mo): ## 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 - for i_blk in 64, for j_blk in 64, for k_blk in 64: # Cache blocking - for i_out, for j_out: - for k_in in 64: - for i_unroll in 2, for j_vec in 16: - C_reg[...] += A[...] * B[...] + # 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 + + # 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 + + # 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} @@ -593,13 +647,40 @@ def _(mo): 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 - for i_blk in 64, for j_blk in 64, for k_blk in 64: - for i_out, for j_out: - for k_in in 64: - for i_unroll in 3, for j_vec in 16: # Optimized Register Tile - C_reg[...] += A[...] * B[...] + # 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 ``` - *Expectation: Maximum Retiring percentage. Peak theoretical performance.* {btn_ex6} """) @@ -630,3 +711,5 @@ def _(btn_ex6, btn_all, mo, run_experiment): # TODO : Rajouter le calcul du peek-perf a chaque exp +# Check pseudo code +# vectorisé k dans l'étape 4 ? From 2115ddaabd55784c78a119e72ceb788734ca7bce Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 24 Jun 2026 06:19:18 +0200 Subject: [PATCH 76/92] [TMA]Show assembly output --- docs/tutorials/hw_counters_introduction_v2.py | 135 ++++-------------- 1 file changed, 30 insertions(+), 105 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index 1fb7918c..14168a4a 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -26,7 +26,7 @@ def _(mo): Optimizing code requires understanding exactly how the CPU executes it. - In this notebook, we will use **XTC** to compile a Matrix Multiplication and evaluate it using hardware counters and Top-down Microarchitecture Analysis (TMA) + 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 @@ -36,7 +36,7 @@ def _(mo, platform): _warnings = [] if platform == "darwin": - _warnings.append(mo.callout(mo.md("**MacOS Detected:** Hardware counters are restricted. The notebook will run, but TMA metrics might be unavailable or require `sudo` privileges."), kind="danger")) + _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:** @@ -251,111 +251,13 @@ def _(mo): -#@app.cell(hide_code=True) -#def _(mo): -# mo.md(r""" -# --- -# ## Step 1: The Naive Implementation -# Let's evaluate a standard, unoptimized Matrix Multiplication. No tiling, no vectorization, no unrolling. -# -# ``` -# for i in 128 -# for j in 512 -# for k in 1024 -# ``` -# -# """) -# return -# -# -# -#@app.cell -#def _(mo, platform): -# -# import xtc.graphs.xtc.op as O -# from xtc.backends.mlir import Backend -# from xtc.schedules.descript import descript_scheduler -# -# I, J, K, dtype = 128, 512, 1024, "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) -# -# # Basic Schedule based on UI inputs -# schedule_spec = ''' -# i: -# j: -# k: -# ''' -# -# -# scheduler = backend.get_scheduler() -# descript_scheduler( -# scheduler=scheduler, -# node_name="C", -# abstract_dims=["i", "j", "k"], -# spec=schedule_spec -# ) -# -# sched = scheduler.schedule() -# -# compiler = backend.get_compiler( -# dump_file="matmul_mlir", -# print_source_ir=False, -# print_transformed_ir=False, -# print_assembly=False, -# shared_lib=True -# ) -# -# module = compiler.compile(sched) -# -# -# # Evaluation -# tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] -# evaluator = module.get_evaluator(validate=False, hw_counters=tma_counters) -# res, code, err = evaluator.evaluate() -# -# # Slicing results -# ex1_l1_res = res[0:4] if len(res) >= 4 else None -# ex1_l2_res = res[4:12] if len(res) >= 12 else None -# ex1_l3_res = res[12:38] if len(res) >= 38 else None -# -# print(f"[DEBUG] ex1 l1 : {ex1_l1_res}") -# print(f"[DEBUG] ex1 l2 : {ex1_l2_res}") -# print(f"[DEBUG] ex1 l3 : {ex1_l3_res}") -# -# -# return I, J, K, a, b, backend, code, dtype, err, evaluator, gb, ex1_l1_res, ex1_l2_res, ex1_l3_res,module, res, schedule_spec, scheduler, tma_counters -# -# -#@app.cell -#def _(err, ex1_l1_res, ex1_l2_res, ex1_l3_res, mo, plot_upipe): -# # Display the upipe diagram -# if err: -# ex1_output = mo.md(f"**Error:**\n```text\n{err}\n```") -# else: -# ex1_output = mo.vstack([ -# mo.md("**Microarchitecture Pipeline Bottlenecks:**"), -# plot_upipe(ex1_l1_res, ex1_l2_res, ex1_l3_res) -# ]) -# -# ex1_output -# return ex1_output, - - - - - @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 + import subprocess # Fixed matrix size for all tests I, J, K, dtype = 512, 512, 512, "float32" @@ -368,9 +270,18 @@ def run_experiment(schedule_spec): backend = Backend(gb.graph) scheduler = backend.get_scheduler() - descript_scheduler(scheduler=scheduler, node_name="C", abstract_dims=["i", "j", "k"], spec=schedule_spec) - - compiler = backend.get_compiler(dump_file="matmul_mlir", shared_lib=True) + descript_scheduler( + scheduler=scheduler, + node_name="C", + abstract_dims=["i", "j", "k"], + spec=schedule_spec + ) + + compiler = backend.get_compiler( + dump_file="matmul_mlir", + shared_lib=True, + print_assembly=True + ) module = compiler.compile(scheduler.schedule()) tma_counters = ["TopdownL1", "TopdownL2", "TopdownL3"] if platform == "linux" else [] @@ -383,9 +294,23 @@ def run_experiment(schedule_spec): 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([ plot_upipe(l1_res, l2_res, l3_res), - mo.accordion({"Click here to see the XTC schedule": mo.md(f"```\n{schedule_spec}\n```")}) + + 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, From 67f2e9a091352f667260c95891e50468e669d8fa Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 24 Jun 2026 06:25:29 +0200 Subject: [PATCH 77/92] [TMA] update tma marimo tutorial [TMA] typo --- docs/tutorials/hw_counters_introduction_v2.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index 14168a4a..b54b37a1 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -1,12 +1,3 @@ -""" -Schema architecture global (caches,front,back...) coloré en fonctions des métriques -Exemple de code de recuperation de compteur avec l'API XTC (une seule partie éditable?) -Tuilage imposé progressif avec le tuto (naif -> ... -> maxi tuilé opti) -Exemple de raté ? (4k aliasing) -Taille de tuile éditable -> possibilité de rentrer les chiffres -""" - - import marimo __generated_with = "0.19.6" @@ -500,8 +491,11 @@ def _(mo): 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 @@ -636,5 +630,3 @@ def _(btn_ex6, btn_all, mo, run_experiment): # TODO : Rajouter le calcul du peek-perf a chaque exp -# Check pseudo code -# vectorisé k dans l'étape 4 ? From 2e2c2bb321e85b42062e024342ff1565d2d3e7ec Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 24 Jun 2026 11:16:49 +0200 Subject: [PATCH 78/92] [TMA] peak perf evaluation --- docs/tutorials/hw_counters_introduction_v2.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index b54b37a1..d1732752 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -248,6 +248,7 @@ 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 @@ -275,10 +276,39 @@ def run_experiment(schedule_spec): ) 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 @@ -295,6 +325,7 @@ def run_experiment(schedule_spec): asm_code = f"Failed to extract assembly: {e}" return mo.vstack([ + perf_ui, plot_upipe(l1_res, l2_res, l3_res), mo.accordion({ From e2d0a4ef323d385b446110901c5f7318c6aa7640 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 24 Jun 2026 16:06:38 +0200 Subject: [PATCH 79/92] [TMA] step 7 --- docs/tutorials/hw_counters_introduction_v2.py | 77 ++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index d1732752..255c3a71 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -266,6 +266,7 @@ def run_experiment(schedule_spec): scheduler=scheduler, node_name="C", abstract_dims=["i", "j", "k"], + abstract_matrix=["A", "B", "C"], spec=schedule_spec ) @@ -654,10 +655,80 @@ def _(btn_ex6, btn_all, mo, run_experiment): run_experiment(spec_6) return spec_6, -if __name__ == "__main__": - app.run() +@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] -# TODO : Rajouter le calcul du peek-perf a chaque exp + # 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 + + + +if __name__ == "__main__": + app.run() From d4ee346bdb1a1ee07bbf7b002931940b7105955a Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 25 Jun 2026 16:32:44 +0200 Subject: [PATCH 80/92] [TMA] sandbox --- docs/tutorials/hw_counters_introduction_v2.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index 255c3a71..c8695efe 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -730,5 +730,151 @@ def _(btn_ex7, btn_all, mo, run_experiment): +@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 _(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() From ea1696f4c8b7a7aa273352c7f59e15d8d8ce6930 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 26 Jun 2026 07:00:27 +0200 Subject: [PATCH 81/92] [TMA] fix warning msg, foldable dictionary & supported arch --- docs/tutorials/hw_counters_introduction.py | 951 ------------------ docs/tutorials/hw_counters_introduction_v2.py | 214 +++- 2 files changed, 213 insertions(+), 952 deletions(-) delete mode 100644 docs/tutorials/hw_counters_introduction.py diff --git a/docs/tutorials/hw_counters_introduction.py b/docs/tutorials/hw_counters_introduction.py deleted file mode 100644 index b8534eed..00000000 --- a/docs/tutorials/hw_counters_introduction.py +++ /dev/null @@ -1,951 +0,0 @@ -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 - -# notebook_dir = os.path.dirname(os.path.realpath(__file__)) -# project_root = os.path.abspath(os.path.join(notebook_dir, "..", "..")) -# os.chdir(project_root) - - -@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. While theoretical complexity (Big O) is useful, modern CPUs are incredibly complex beasts with deep pipelines, multiple cache levels, and speculative execution. - - In this notebook, we will use **XTC** to compile a Matrix Multiplication and evaluate it using: - 1. **Raw PMU Counters** (Performance Monitoring Units) to count exact hardware events. - 2. **Top-down Microarchitecture Analysis (TMA)** to identify actual bottlenecks. - """) - return - - -@app.cell(hide_code=True) -def _(mo): - mo.md(r""" - > **Disclaimer and prerequisites:** - > Results will vary depending on your hardware architecture (MacOS is currently not supported). - > If your microarchitecture is not explicitly mapped by the internal C resolver, the system will gracefully fallback to the `perf stat` command-line tool. - > - > To make hardware counters available to userspace applications (ring 3), run this in your terminal: - > ```bash - > sudo sysctl kernel.perf_event_paranoid=-1 - > echo 0 | sudo tee /proc/sys/kernel/nmi_watchdog # not needed on ARM - > ``` - """) - return - - -@app.cell(hide_code=True) -def _(mo): - mo.md(r""" - ## 1. Defining the Workload & Schedule - We define a medium-sized matrix multiplication (1024x2048x4096). - - **Interactive Workflow:** - 1. Adjust the sliders in the sidebar to update the tile size. - 2. Click the evaluation buttons below each section to run the PMU/TMA counters on demand. - """) - return - - -@app.cell -def _(mo): - # UI sliders - tile_i_ui = mo.ui.slider(start=4, stop=256, step=4, value=4, label="Tile I (Rows)") - tile_j_ui = mo.ui.slider(start=16, stop=512, step=16, value=16, label="Tile J (Cols)") - unroll_ui = mo.ui.slider(start=1, stop=128, step=1, value=2, label="Unroll factor") - return tile_i_ui, tile_j_ui, unroll_ui - - -@app.cell -def _(mo): - _editor_code = '''import xtc.graphs.xtc.op as O - from xtc.backends.mlir import Backend - from xtc.schedules.descript import descript_scheduler - - # Problem setup - I, J, K, dtype = 1024, 2048, 4096, "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) - - # Schedule specification - schedule_spec = { - "i": {}, - "k": {}, - "j": {}, - f"i#{slider_i}": {"unroll": slider_unroll}, - f"j#{slider_j}": {"vectorize": True} - } - - # Compile - scheduler = backend.get_scheduler() - descript_scheduler( - scheduler=scheduler, - node_name="C", - abstract_dims=["i", "j", "k"], - spec=schedule_spec - ) - sched = scheduler.schedule() - - compiler = backend.get_compiler( - dump_file="matmul_mlir", - print_source_ir=False, - print_transformed_ir=False, - print_assembly=False, - shared_lib=True - ) - module = compiler.compile(sched) - ''' - descript_editor = mo.ui.code_editor( - value=_editor_code, - language="python", - label="Schedule & Compilation Sandbox" - ) - return (descript_editor,) - - -@app.cell -def _(descript_editor): - descript_editor - return - - -@app.cell -def _(descript_editor, tile_i_ui, tile_j_ui, unroll_ui): - # Execute user code safely - local_vars = {} - - # Inject slider values into execution context - global_vars = globals().copy() - global_vars.update({ - "slider_i": tile_i_ui.value, - "slider_j": tile_j_ui.value, - "slider_unroll": unroll_ui.value, - }) - - exec_error = None - module = None - I_val, J_val, K_val = 1024, 2048, 4096 - - try: - exec(descript_editor.value, global_vars, local_vars) - module = local_vars.get("module") - I_val = local_vars.get("I", 1024) - J_val = local_vars.get("J", 2048) - K_val = local_vars.get("K", 4096) - except Exception as e: - exec_error = str(e) - return I_val, J_val, K_val, exec_error, module - - -@app.cell -def _(I_val, J_val, K_val, mo, os, sys, tile_i_ui, tile_j_ui, unroll_ui): - # Fetch hardware cache sizes dynamically (sysfs) - caches_kb = {} - if sys.platform == "linux" and os.path.exists("/sys/devices/system/cpu/cpu0/cache"): - try: - for i in range(4): - path = f"/sys/devices/system/cpu/cpu0/cache/index{i}" - if os.path.exists(path): - with open(f"{path}/level", "r") as f: - level = int(f.read().strip()) - with open(f"{path}/type", "r") as f: - ctype = f.read().strip() - with open(f"{path}/size", "r") as f: - size_str = f.read().strip() - - s_kb = 0 - if size_str.endswith('K'): s_kb = int(size_str[:-1]) - elif size_str.endswith('M'): s_kb = int(size_str[:-1]) * 1024 - else: s_kb = int(size_str) // 1024 - - if level == 1 and ctype == "Data": caches_kb["L1d"] = s_kb - elif level == 2: caches_kb["L2"] = s_kb - elif level == 3: caches_kb["L3"] = s_kb - except Exception: - pass - - # Defaults if reading fails - if "L1d" not in caches_kb: caches_kb["L1d"] = 1 - if "L2" not in caches_kb: caches_kb["L2"] = 1 - if len(caches_kb) <= 2 and "L3" not in caches_kb: caches_kb["L3"] = 1 - - b_size = 4 - - def fmt(b): - if b >= 1024**2: return f"{b / 1024**2:.1f} MiB" - if b >= 1024: return f"{b / 1024:.1f} KiB" - return f"{b} B" - - geo_md = f""" - | Tensor | Total Size | 1 Row | 1 Col | - |---|---|---|---| - | **A** ({I_val}×{K_val}) | {fmt(I_val*K_val*b_size)} | {fmt(K_val*b_size)} | {fmt(I_val*b_size)} | - | **B** ({K_val}×{J_val}) | {fmt(K_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(K_val*b_size)} | - | **C** ({I_val}×{J_val}) | {fmt(I_val*J_val*b_size)} | {fmt(J_val*b_size)} | {fmt(I_val*b_size)} | - """ - - tile_c_bytes = tile_i_ui.value * tile_j_ui.value * b_size - flops = 2 * I_val * J_val * K_val - gflops = flops / 1e9 - - def format_cache(name, kb): - mb = kb / 1024 - if kb >= 1024: return f"**{name}** : {mb:.1f} MiB" - return f"**{name}** : {kb:.0f} KiB" - - cache_lines = [ - format_cache("L1 Data", caches_kb.get("L1d", 32)), - format_cache("L2 Cache", caches_kb.get("L2", 1024)) - ] - if "L3" in caches_kb: - cache_lines.append(format_cache("L3 Cache", caches_kb["L3"])) - - cache_md = "
".join([f" {l}" for l in cache_lines]) - - sidebar = mo.sidebar( - mo.vstack([ - mo.md("### Schedule Controls"), - tile_i_ui, - tile_j_ui, - unroll_ui, - mo.md("---"), - mo.md("### Problem Geometry"), - mo.md(geo_md), - mo.md(f"**Total Math:** `{gflops:.1f} GFLOPs`"), - mo.md(f"**Current C-Tile (i×j):** `{fmt(tile_c_bytes)}`"), - mo.md("---"), - mo.md("### CPU Caches"), - mo.md("
*Compare row/col sizes to your caches to predict bottlenecks!*"), - mo.md(cache_md) - ]) - ) - return (sidebar,) - - -@app.cell -def _(sidebar): - sidebar - return - - -@app.cell -def _(mo): - # On-demand execution buttons - btn_pmu = mo.ui.run_button(kind="info", label="Evaluate Raw PMUs") - btn_l1 = mo.ui.run_button(kind="info", label="Evaluate Topdown L1") - btn_l2 = mo.ui.run_button(kind="info", label="Evaluate Topdown L2") - - _sandbox_default = '[\n "instructions",\n "branches",\n "branch-misses",\n "L1-dcache-loads",\n "L1-dcache-load-misses",\n "TopdownL1",\n "TopdownL2",\n "TopdownL3"\n]' - sandbox_editor = mo.ui.code_editor(value=_sandbox_default, language="python") - btn_sandbox = mo.ui.run_button(kind="success", label="Run Custom Metrics") - return btn_l1, btn_l2, btn_pmu, btn_sandbox, sandbox_editor - - -@app.cell(hide_code=True) -def _(btn_pmu, mo): - mo.md(f""" - ## 2. Raw Hardware Counters (PMU) - CPUs expose raw counters to track specific events. We can ask the CPU exactly how many cycles were spent or how many L1/L2 cache misses occurred. - - *Note: Raw event names are highly architecture-dependent. `libpfm4` helps translating them. The `perf list` command can show you available ones.* - - {btn_pmu} - """) - - -@app.cell -def _(btn_pmu, exec_error, mo, module): - mo.stop(not btn_pmu.value, mo.md("*Click the button above to execute PMU Evaluation.*")) - - if module is None: - pmu_ui = mo.md(f"**Compilation Error:**\n```python\n{exec_error}\n```") - else: - pmu_counters = ["cycles", "instructions", "cache_access", "cache_misses", "branches", "branches_misses"] - - evaluator_pmu = module.get_evaluator(validate=True, hw_counters=pmu_counters) - results_pmu, code_pmu, error_pmu = evaluator_pmu.evaluate() - - _pmu_data = [{"Counter": c, "Value": "Fallback needed" if v == -1.0 else str(int(v))} for c, v in zip(pmu_counters, results_pmu)] - pmu_ui = mo.vstack([ - mo.md(f"**Execution Code:** `{code_pmu}`"), - mo.ui.table(_pmu_data, label="Raw PMU Results") - ]) - - pmu_ui - return - - -@app.cell(hide_code=True) -def _(btn_l1, mo): - mo.md(f""" - ## 3. Top-down Microarchitecture Analysis (Level 1) - Raw counters are hard to interpret. To solve this, **TMA** (Top-down Analysis) groups all CPU pipeline slots into 4 distinct categories, summing up to 100%: - - To solve this, **TMA** (Top-down Analysis) groups all CPU pipeline slots into 4 distinct categories, summing up to 100%: - * 🟢 **Retiring:** Good! The CPU is doing useful work (executing our math). - * 🔴 **Bad Speculation:** The CPU guessed a branch wrong and had to flush its pipeline. - * 🔵 **Frontend Bound:** The CPU is starved; it cannot fetch/decode instructions fast enough. - * 🟣 **Backend Bound:** The CPU is waiting (usually for Memory or Execution Units) to finish the current instructions. - - {btn_l1} - """) - - -@app.cell -def _(btn_l1, exec_error, mo, module, platform): - mo.stop(not btn_l1.value, mo.md("*Click the button above to execute Topdown L1 Evaluation.*")) - - if module is None: - l1_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") - else: - tma_l1_counters = ["TopdownL1"] if platform == "linux" else [] - evaluator_l1 = module.get_evaluator(validate=False, hw_counters=tma_l1_counters) - results_l1, code_l1, error_l1 = evaluator_l1.evaluate() - - _l1_labels = ["🟢 Retiring", "🔴 Bad Speculation", "🔵 Frontend Bound", "🟣 Backend Bound"] - - if tma_l1_counters and len(results_l1) > 0: - if results_l1[0] < 0: - _l1_ui_display = mo.accordion({"Arch not supported or hardware counters not activated. Fallback to `perf stat` output": mo.md(f"```text\n{error_l1}\n```")}) - else: - _l1_data = [{"Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l1_labels, results_l1)] - _l1_data.sort(key=lambda x: x["Percentage (%)"], reverse=True) - _l1_ui_table = mo.ui.table(_l1_data, label="Topdown L1 Results") - - try: - import altair as alt - - _chart = alt.Chart(alt.Data(values=_l1_data)).mark_arc(innerRadius=50).encode( - theta=alt.Theta(field="Percentage (%)", type="quantitative"), - color=alt.Color( - field="Category", - type="nominal", - scale=alt.Scale( - domain=_l1_labels, - range=["#4ade80", "#ff4a4a", "#60a5fa", "#c084fc"] # Vert, Rouge, Bleu, Violet - ), - legend=alt.Legend(title="Bottlenecks") - ), - tooltip=["Category:N", "Percentage (%):Q"], - order=alt.Order("Percentage (%):Q", sort="descending"), - ).properties(width=300, height=300, title="TMA L1 Breakdown") - - _l1_ui_display = mo.hstack([_l1_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) - - except ImportError: - _l1_ui_display = mo.vstack([ - mo.md("*Tip: Install `altair` (`pip install altair`) to see the interactive pie chart!*"), - _l1_ui_table - ]) - else: - _l1_ui_display = mo.md("*TMA L1 is not supported on this machine.*") - - l1_ui = mo.vstack([ - mo.md(f"**Execution Code:** `{code_l1}`"), - _l1_ui_display - ]) - - l1_ui - return evaluator_l1, l1_ui, results_l1, tma_l1_counters - - -@app.cell -def _(btn_l2, mo): - mo.md(f""" - ## 4. Drilling Down (Topdown Level 2) - If our code is heavily **Backend Bound**, we need to know why. TMA Level 2 splits the L1 categories further: - - * **Core Bound:** Lack of execution units (ALU/FPU) or data dependencies between instructions. - * **Memory Bound:** Execution Units are starved because of non-completed in-flight memory demand loads. - - **Analyzing Retiring:** - If **🟢 Retiring: Light Ops** is unusually high, it often indicates inefficient vectorization. - This typically occurs when matrix dimensions are not perfectly divisible by your tile sizes, - forcing the compiler to generate scalar instructions for the remaining elements (loop tails). - You can verify if the backend successfully leverages wide vector registers (like `ymm` or `zmm`) - by inspecting the generated assembly. - - - {btn_l2} - """) - return - -@app.cell -def _(mo): - check_asm_content = mo.md( - """ - - Set the flag **print_assembly** to **True** in the code editor cell. - - The output will be in the Marimo's server terminal the next compilation of the module. - """ - ) - - check_asm_msg = mo.accordion({ - "💡 How to see assembly output": check_asm_content - }) - return (check_asm_msg,) - -@app.cell -def _(check_asm_msg): - check_asm_msg - return - -@app.cell -def _(btn_l2, exec_error, mo, module, platform): - mo.stop(not btn_l2.value, mo.md("*Click the button above to execute Topdown L2 Evaluation.*")) - - if module is None: - l2_ui = mo.md(f"**Compilation Error in Sandbox:**\n```python\n{exec_error}\n```") - else: - tma_l2_counters = ["TopdownL2"] if platform == "linux" else [] - evaluator_l2 = module.get_evaluator(validate=False, hw_counters=tma_l2_counters) - results_l2, code_l2, error_l2 = evaluator_l2.evaluate() - - _l2_labels = [ - "🟢 Retiring: Light Ops", - "🟢 Retiring: Heavy Ops", - "🔴 Bad Spec: Machine Clears", - "🔴 Bad Spec: Branch Mispredicts", - "🔵 Frontend: Fetch Bandwidth", - "🔵 Frontend: Fetch Latency", - "🟣 Backend: Core Bound", - "🟣 Backend: Memory Bound" - ] - - if tma_l2_counters and len(results_l2) > 0: - if results_l2[0] < 0: - _l2_ui_display = mo.vstack([ - mo.callout(mo.md("*Hardware counter not activated or internal C resolver unsupported for L2. Showing `perf stat` output below.*"), kind="warn"), - mo.accordion({"Terminal Output from perf": mo.md(f"```text\n{error_l2}\n```")}) - ]) - else: - _l2_data = [{"Sub-Category": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l2_labels, results_l2)] - _l2_data.sort(key=lambda x: x["Percentage (%)"], reverse=True) - _l2_ui_table = mo.ui.table(_l2_data, label="Topdown L2 Results") - try: - import altair as alt2 - - _chart = alt2.Chart(alt2.Data(values=_l2_data)).mark_arc(innerRadius=50).encode( - theta=alt2.Theta(field="Percentage (%)", type="quantitative"), - color=alt2.Color( - field="Sub-Category", - type="nominal", - scale=alt2.Scale( - domain=_l2_labels, - range=[ - "#4ade80", "#22c55e", # light green, deep green (Retiring) - "#f87171", "#ef4444", # light red, deep red (Bad Spec) - "#60a5fa", "#3b82f6", # light blue, deep blue (Frontend) - "#c084fc", "#a855f7" # light purple, deep purple (Backend) - ] - ), - legend=alt2.Legend(title="Sub-Bottlenecks") - ), - tooltip=["Sub-Category:N", "Percentage (%):Q"], - order=alt2.Order("Percentage (%):Q", sort="descending") - ).properties(width=350, height=300, title="TMA L2 Breakdown") - - _l2_ui_display = mo.hstack([_l2_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) - - except ImportError: - _l2_ui_display = mo.vstack([ - mo.md("*Tip: Install `altair` (`pip install altair`) to see the interactive pie chart!*"), - _l2_ui_table - ]) - else: - _l2_ui_display = mo.md("*TMA L2 is not supported on this machine.*") - - l2_ui = mo.vstack([mo.md(f"**Execution Code:** `{code_l2}`"), _l2_ui_display]) - - l2_ui - return evaluator_l2, l2_ui, results_l2, tma_l2_counters - - -@app.cell(hide_code=True) -def _(mo): - mo.md(r""" - ## How to fix Core Bound vs Memory Bound? - - ### If you are heavily Core Bound: - Your CPU has all the data it needs in the caches, but it cannot crunch the numbers fast enough. - * **Vectorization:** Ensure your loops are properly vectorized so the CPU processes 8 or 16 floats per instruction (AVX2/AVX-512) instead of 1. - * **Instruction-Level Parallelism:** Increase the `unroll` factor. This breaks data dependency chains and allows the CPU's Out-Of-Order engine to execute multiple independent additions/multiplications at the exact same time. - - ### If you are heavily Memory Bound: - Your CPU's execution units are starving because they are waiting hundreds of cycles for data to arrive from RAM. - * **Cache Locality:** Modify your `tile` sizes (Tiling/Blocking). The goal is to make sure a chunk of Matrix A and Matrix B fits perfectly inside the ultra-fast L1 or L2 cache before computing it. - * **Memory Access Pattern:** Ensure contiguous memory accesses. If your code jumps around memory (strided access), the hardware prefetcher cannot predict what to load next. - """) - return - - -@app.cell(hide_code=True) -def _(btn_sandbox, mo, sandbox_editor): - mo.vstack([ - mo.md(r""" - --- - ## Sandbox: Experiment with your own metrics - - Curious about branch misses or raw L1 cache loads? Write your own list of PMU/TMA events below. - - > **/!\\ Important Hardware Limits:** - - > CPUs only has a few programmable counters (usually 4 or 8). - - > * If you ask for too many events, the CPU will run out of hardware counters. Linux will silently disable the overflowing ones. - - > * Overflowing events from somes TMA can be handle by a multi-passes mechanic in XTC.* - - > * **Symptom:** You will see mathematically aberrant data, such as `[-0.0, ..., -0.0]` or an absurdly high ones.If you see this, shorten your list! - - - *This cell will not run automatically. You must click the button to evaluate the kernel.* - """), - sandbox_editor, - btn_sandbox - ]) - 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( - """ - *Note: Returned as named attributes via the `perf` fallback.* - - | 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( - """ - *Note: Returned as named attributes via the `perf` fallback.* - - | 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( - """ - *Note: Returned as named attributes via the `perf` fallback.* - - | 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:** Native C API returns arrays for L1, L2 and L3. L4 and beyond use the `perf` fallback output."), - mo.accordion({ - "Level 1 (4 metrics - Array output)": l1_md, - "Level 2 (8 metrics - Array output)": l2_md, - "Level 3 (26 metrics - Array output)": l3_md, - "Level 4 (32 metrics - perf fallback)": l4_md, - "Level 5 (15 metrics - perf fallback)": l5_md, - "Level 6 (8 metrics - perf fallback)": l6_md - }) - ]) - - return reminder_output_msg, - -@app.cell -def _(reminder_output_msg): - reminder_output_msg - return - - -@app.cell -def _(supported_arch_msg): - supported_arch_msg - return - - -@app.cell -def _(btn_sandbox, mo, module, sandbox_editor): - mo.stop(not btn_sandbox.value, mo.md("*Click 'Run Custom Metrics' to see the results.*")) - - if module is None: - sandbox_output = mo.md("**Module is not compiled.** Please fix the compilation sandbox above first.") - else: - import ast - try: - custom_counters = ast.literal_eval(sandbox_editor.value) - if not isinstance(custom_counters, list): raise ValueError("Input must be a Python list.") - - evaluator_sb = module.get_evaluator(validate=False, hw_counters=custom_counters) - results_sb, code_sb, err_sb = evaluator_sb.evaluate() - - output_lines = [f"**Execution Code:** `{code_sb}`\n"] - - - output_lines.append("**Raw Results:**\n```text") - - - DERIVED_METRICS_SIZES = {"TopdownL1": 4, "TopdownL2": 8, "TopdownL3": 26, "TopdownL4": 32, "TopdownL5": 15, "TopdownL6": 8} - current_idx = 0 - - for c in custom_counters: - size = DERIVED_METRICS_SIZES.get(c, 1) - chunk = results_sb[current_idx : current_idx + size] - - if size == 1: - output_lines.append(f"{c:35} : {int(chunk[0])}") - else: - rounded_chunk = [round(x, 2) for x in chunk] - output_lines.append(f"{c:35} : {rounded_chunk}") - current_idx += size - - output_lines.append("```") - - if err_sb: - output_lines.append(f"**Terminal Output / Errors:**\n```text\n{err_sb}\n```") - - sandbox_output = mo.md("\n".join(output_lines)) - - except Exception as e: - sandbox_output = mo.md(f"**Error parsing your list:** {e}") - - sandbox_output - return - - -@app.cell(hide_code=True) -def _(mo): - btn_l3 = mo.ui.run_button(kind="neutral", label="Evaluate Topdown L3") - btn_asm = mo.ui.run_button(kind="neutral", label="Analyze Assembly (Spilling)") - - unroll_md = mo.md(f""" - --- - - ## 5. The "Register Pressure" Wall (Spilling) - - In optimization, "more" is not always better. Let's demonstrate **Register Pressure**. - - Modern CPUs have a limited number of physical vector registers (e.g., 16 for AVX2, 32 for AVX-512). These registers act as the "L0 Cache" and can hold a very small matrix block (e.g., 4x16). If your inner tile sizes (`Tile I`, `Tile J`) or your `Unroll` factor are too large, the compiler runs out of physical registers to store intermediate accumulations. It is forced to "spill" them to the stack (which lives in the L1 Cache). - - **The Experiment:** - 1. Set `Tile I` to **4**, `Tile J` to **32**, and `Unroll` to **2**. Click both buttons below. You should see a healthy Math/Memory ratio in the assembly. - 2. Now, set `Tile I` to **64** and `Tile J` to **64**. Click both buttons again. - - You should see the **`L1 Bound`** explode in the Topdown L3 chart, and a massive spike of memory operations (`vmovups`, `mov`, etc.) dominating the assembly instruction table! The CPU is drowning in L1 Cache transfers because the micro-tile no longer fits in its physical registers. - - If the tile is way too big, the bottleneck will be reported to **`L2 bound`** then **`L3 bound`** ... - - {mo.hstack([btn_l3, btn_asm])} - """) - - return btn_asm, btn_l3, unroll_md - - -@app.cell -def _(btn_l3, exec_error, mo, module, platform): - mo.stop(not btn_l3.value, mo.md("*Click 'Evaluate Topdown L3' to execute.*")) - - if module is None: - l3_ui = mo.md(f"**Compilation Error:**\n```python\n{exec_error}\n```") - else: - tma_l3_counters = ["TopdownL3"] if platform == "linux" else [] - evaluator_l3 = module.get_evaluator(validate=False, hw_counters=tma_l3_counters) - results_l3, code_l3, error_l3 = evaluator_l3.evaluate() - - _l3_labels = [ - "Branch Resteers", "Divider", "DRAM Bound", "DSB", "DSB Switches", "Few Uops", - "FP Arith", "Fused", "ICache Misses", "ITLB Misses", "L1 Bound", "L2 Bound", - "L3 Bound", "LCP", "Memory Ops", "Microcode Seq", "MITE", "MS Switches", - "Non-Fused Branches", "Other Light Ops", "Other Mispredicts", "Other Nukes", - "PMM Bound", "Ports Utilization", "Serializing", "Store Bound" - ] - - if tma_l3_counters and len(results_l3) > 0: - if results_l3[0] < 0: - l3_ui_display = mo.vstack([ - mo.md("*Native TopdownL3 unsupported. Showing fallback output.*"), - mo.accordion({"Fallback output": mo.md(f"```text\n{error_l3}\n```")}) - ]) - else: - _l3_data = [{"Metric": l, "Percentage (%)": round(v, 2)} for l, v in zip(_l3_labels, results_l3) if v > 1.0] - - # move metrics < 1% into "others" - _other_sum = sum(v for v in results_l3 if v <= 1.0) - if _other_sum > 0: - _l3_data.append({"Metric": "Others (<1%)", "Percentage (%)": round(_other_sum, 2)}) - - _l3_data.sort(key=lambda x: x["Percentage (%)"], reverse=True) - _l3_ui_table = mo.ui.table(_l3_data, label="Topdown L3 (Metrics >1%)") - - try: - import altair as alt3 - color_condition = alt3.condition( - alt3.datum.Metric == 'L1 Bound', - alt3.value('#ef4444'), - alt3.Color('Metric:N') - ) - - _chart = alt3.Chart(alt3.Data(values=_l3_data)).mark_arc(innerRadius=40).encode( - theta=alt3.Theta(field="Percentage (%)", type="quantitative"), - color=color_condition, - order=alt3.Order("Percentage (%):Q", sort="descending"), - tooltip=["Metric:N", "Percentage (%):Q"] - ).properties(width=350, height=300, title="TMA L3 Breakdown") - - l3_ui_display = mo.hstack([_l3_ui_table, mo.ui.altair_chart(_chart)], justify="start", gap=4) - except ImportError: - l3_ui_display = _l3_ui_table - else: - l3_ui_display = mo.md("*TMA L3 is not supported on this machine.*") - - l3_ui = mo.vstack([mo.md(f"**Execution Code:** `{code_l3}`"), l3_ui_display]) - - return evaluator_l3, l3_ui, results_l3, tma_l3_counters - - -@app.cell -def _(mo,unroll_md): - unroll_md - return - -@app.cell -def _(mo,l3_ui): - l3_ui - return - -@app.cell -def _(btn_asm, mo, module): - mo.stop(not btn_asm.value, mo.md("*Click 'Analyze Assembly' to parse instructions.*")) - - so_path = getattr(module, "file_name", None) - if not so_path: - asm_ui = mo.md("**Module shared object (.so) not available for analysis.**") - else: - import subprocess - from collections import Counter - - try: - out = subprocess.check_output(["objdump", "-d", so_path], universal_newlines=True) - - counter = Counter() - mem_instr = 0 - math_instr = 0 - spill_count = 0 - - for line in out.splitlines(): - if ":" in line: - parts = line.split("\t") - if len(parts) >= 3: - instr_full = parts[2].strip() - mnemonic = instr_full.split()[0] - operands = instr_full[len(mnemonic):] - - if mnemonic.isalnum(): - counter[mnemonic] += 1 - - if "mov" in mnemonic or "ldr" in mnemonic or "str" in mnemonic: - mem_instr += 1 - # Spilling on stack - if "%rsp" in operands or "%rbp" in operands: - spill_count += 1 - print(f"[DEBUG] Spill at : {mnemonic} {operands}") - elif "add" in mnemonic or "mul" in mnemonic or "fma" in mnemonic: - math_instr += 1 - - if not counter: - asm_ui = mo.md("*Could not parse assembly instructions from the binary.*") - else: - asm_data = [{"Mnemonic": k, "Occurrences": v} for k, v in counter.most_common(20)] - ratio = mem_instr / max(math_instr, 1) - - alerts = [] - # Avoid the absolute value put at the end of the function - if spill_count > 1: - alerts.append(mo.callout(mo.md(f"**REGISTER SPILLING DETECTED!** Found **{spill_count}** memory operations hitting the stack (`%rsp` or `%rbp`). The compiler ran out of physical vector registers!"), kind="danger")) - elif ratio > 1.5: - alerts.append(mo.callout(mo.md(f"**High Memory Traffic ({ratio:.1f}x).** No stack spills detected, but the loop is drowning in memory operations. The compiler probably failed to keep the accumulator matrix 'C' in registers (missing Load/Store Hoisting)."), kind="warn")) - else: - alerts.append(mo.callout(mo.md(f"**Healthy Memory/Math Ratio ({ratio:.1f}x).** Accumulators are efficiently kept in registers."), kind="success")) - - asm_ui = mo.vstack(alerts + [mo.ui.table(asm_data, label="Top 20 Instructions")]) - - except Exception as e: - asm_ui = mo.md(f"*Failed to run objdump: {e}*") - - return asm_ui, - -@app.cell -def _(mo,asm_ui): - asm_ui - return - - -if __name__ == "__main__": - app.run() diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index c8695efe..1e19e2d7 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -41,6 +41,7 @@ def _(mo, platform): """), kind="warn")) prerequisites_ui = mo.vstack(_warnings) + prerequisites_ui return prerequisites_ui, @@ -49,12 +50,23 @@ 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. + ### 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): @@ -819,6 +831,206 @@ def _(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.*")) From 1d811541e911d60b5a01f0fbd7ccbd6472a7e915 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 26 Jun 2026 08:49:59 +0200 Subject: [PATCH 82/92] [TMA] remove debug output --- .../evaluation/test_matmul_tma_counters.py | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/tests/filecheck/evaluation/test_matmul_tma_counters.py b/tests/filecheck/evaluation/test_matmul_tma_counters.py index ea3b006e..b6b2c072 100644 --- a/tests/filecheck/evaluation/test_matmul_tma_counters.py +++ b/tests/filecheck/evaluation/test_matmul_tma_counters.py @@ -71,57 +71,6 @@ print(f"{'counters'}: {hw_counters}") print(f"{'results'}: {[round(x, 2) for x in results]}") -if len(results) >= 17: - use_colors = sys.stdout.isatty() - - RED = "\033[91m" if use_colors else "" - ORANGE = "\033[38;5;208m" if use_colors else "" - YELLOW = "\033[93m" if use_colors else "" - GREEN = "\033[92m" if use_colors else "" - MAGENTA = "\033[95m" if use_colors else "" - RESET = "\033[0m" if use_colors else "" - - def get_c(val): - if val > 90: return RED - if val > 75: return ORANGE - if val > 50: return YELLOW - if val > 10: return GREEN - # < 10 white - if val == -1: return MAGENTA - return RESET - - w = 25 - - print("-" * (w + 10)) - - # L1 Metrics - print(f"{'L1 Retiring':<{w}}: {get_c(results[0])}{results[0]:.2f}{RESET}") - print(f"{'L1 Bad speculation':<{w}}: {get_c(results[1])}{results[1]:.2f}{RESET}") - print(f"{'L1 Frontend bound':<{w}}: {get_c(results[2])}{results[2]:.2f}{RESET}") - print(f"{'L1 Backend bound':<{w}}: {get_c(results[3])}{results[3]:.2f}{RESET}") - - print("") - # L2 Metrics - print(f"{'L2 Light ops':<{w}}: {get_c(results[4])}{results[4]:.2f}{RESET}") - print(f"{'L2 Heavy ops':<{w}}: {get_c(results[5])}{results[5]:.2f}{RESET}") - print(f"{'L2 Machine clear':<{w}}: {get_c(results[6])}{results[6]:.2f}{RESET}") - print(f"{'L2 Branch misspredict':<{w}}: {get_c(results[7])}{results[7]:.2f}{RESET}") - print(f"{'L2 Fetch bandwidth':<{w}}: {get_c(results[8])}{results[8]:.2f}{RESET}") - print(f"{'L2 Fetch latency':<{w}}: {get_c(results[9])}{results[9]:.2f}{RESET}") - print(f"{'L2 Core bound':<{w}}: {get_c(results[10])}{results[10]:.2f}{RESET}") - print(f"{'L2 Memory bound':<{w}}: {get_c(results[11])}{results[11]:.2f}{RESET}") - - print("") - # L3 Memory Metrics - print(f"{'L3 L1 bound':<{w}}: {get_c(results[12])}{results[12]:.2f}{RESET}") - print(f"{'L3 L2 bound':<{w}}: {get_c(results[13])}{results[13]:.2f}{RESET}") - print(f"{'L3 L3 bound':<{w}}: {get_c(results[14])}{results[14]:.2f}{RESET}") - print(f"{'L3 DRAM bound':<{w}}: {get_c(results[15])}{results[15]:.2f}{RESET}") - print(f"{'L3 store bound':<{w}}: {get_c(results[16])}{results[16]:.2f}{RESET}") - - - - # CHECK: CODE: 0 # CHECK-NEXT: counters: # CHECK-NEXT: results: From 48ce327f106d680ae7ca6733c10fbca25a888628 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 26 Jun 2026 14:58:00 +0200 Subject: [PATCH 83/92] [TMA]opti: only check cpu model once + refactor tma implementation --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 493 +++++++-------------- src/xtc/utils/evaluation.py | 145 +++--- 2 files changed, 238 insertions(+), 400 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 3525a72c..14c0e60f 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -28,125 +28,7 @@ #if ARCH_IS_X86 #include -#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 { - INTEL_UNKNOWN = 0, - INTEL_SKYLAKE_CASCADE, - INTEL_ICELAKE_SAPPHIRE -} intel_arch_t; -intel_arch_t detect_intel_microarchitecture(void) { -#if ARCH_IS_X86 - int family, model; - get_cpu_family_model(&family, &model); - - // source : https://github.com/torvalds/linux/blob/ma ster/arch/x86/include/asm/intel-family.h - - if (family == 6) { - switch (model) { - // Arch without TMA counter - case 0x4E: case 0x5E: case 0x55: // Skylake-X / Cascade Lake-X / Cooper Lake - case 0x8E: case 0x9E: // Whiskey Lake / Kaby Lake / Coffee Lake / Amber lake - case 0xA5: case 0xA6: // Comet Lake - case 0x3D: case 0x47: case 0x4F: // Broadwell - case 0x56: // Broadwell-DE - case 0x66: // Cannon Lake - return INTEL_SKYLAKE_CASCADE; - - // Arch with TMA counter - case 0x7E: case 0x7D: case 0x9D: // Ice Lake Client - case 0x6A: case 0x6C: // Ice Lake Server (Xeon) - case 0xA7: // Rocket Lake (Cypress Cove) - case 0x8C: case 0x8D: // Tiger Lake - case 0x8A: // Lakefield - case 0x8F: // Sapphire Rapids (Xeon) - case 0xCF: // Emerald Rapids (Xeon) - case 0xAD: case 0xAE: // Granite Rapids (Xeon X et D) - case 0x97: case 0x9A: // Alder Lake (12th Gen) - case 0xB7: case 0xBA: case 0xBF: // Raptor Lake (13th/14th Gen) - case 0xD7: // Bartlett Lake - case 0xAA: case 0xAC: // Meteor Lake (Core Ultra 1) - case 0xB5: case 0xC5: case 0xC6: // Arrow Lake - case 0xBD: // Lunar Lake (Core Ultra 2) - case 0xCC: case 0xE5: // Panther Lake - case 0xD5: // Wildcat Lake - return INTEL_ICELAKE_SAPPHIRE; - } - } - else if (family == 18) { - switch (model) { - case 0x01: case 0x03: // Nova Lake - return INTEL_ICELAKE_SAPPHIRE; - } - } - else if (family == 19) { - switch (model) { - case 0x01: // Diamond Rapids (Xeon) - return INTEL_ICELAKE_SAPPHIRE; - } - } -#endif - return INTEL_UNKNOWN; -} - -typedef enum { - AMD_UNKNOWN = 0, - AMD_ZEN_1_2, - AMD_ZEN_3, - AMD_ZEN_4 -} amd_arch_t; - -amd_arch_t detect_amd_microarchitecture(void) { -#if ARCH_IS_X86 - int family, model; - get_cpu_family_model(&family, &model); - - if (family == 0x17) { - // Zen 1 : Naples, Summit Ridge - // Zen+ : Pinnacle Ridge - // Zen 2 : Rome, Matisse, Naples - return AMD_ZEN_1_2; - } - - if (family == 0x19) { - if (model >= 0x10 && model <= 0x1F) return AMD_ZEN_4; // Genoa - if (model >= 0x60 && model <= 0x7F) return AMD_ZEN_4; // Phoenix/Dragon Range - if (model >= 0xA0 && model <= 0xAF) return AMD_ZEN_4; // Bergamo - - // Todo : double check if missing models - return AMD_ZEN_3; // (Milan, Vermeer...) - } - // todo family 0x1A for Zen 5 (Turin, Granite Ridge) -#endif - return AMD_UNKNOWN; -} #define GET_METRIC(m, i) (((m) >> (i*8)) & 0xff) @@ -187,50 +69,128 @@ call. */ - -int detect_if_intel(void) { -#if ARCH_IS_X86 +#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(0, &eax, &ebx, &ecx, &edx)) { - // ebx = "Genu" (0x756e6547) - // edx = "ineI" (0x49656e69) - // ecx = "ntel" (0x6c65746e) - if (ebx == 0x756e6547 && edx == 0x49656e69 && ecx == 0x6c65746e) { - return 1; + + 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 - return 0; } +#endif -int detect_if_amd(void) { +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; - if (__get_cpuid(0, &eax, &ebx, &ecx, &edx)) { - // ebx = "Auth" (0x68747541) - // edx = "enti" (0x69746e65) - // ecx = "cAMD" (0x444d4163) - if (ebx == 0x68747541 && edx == 0x69746e65 && ecx == 0x444d4163) { - return 1; + 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 0; + return UARCH_UNKNOWN; } -int detect_if_arm(void) { - return ARCH_IS_ARM; +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; } - -static const int pass_1[] = {1}; -static const int pass_4[] = {4}; -static const int pass_5[] = {5}; -static const int pass_9[] = {9}; - - -// === Skylake arch === +// Skylake arch static const char *skl_tma_l1_events[] = { "@skl_slots", // 0x003c (Cycles) @@ -552,7 +512,7 @@ static void compute_skl_tma_l3(const double *raw, double *final) { } } -// === Modern Inter arch === +// Modern Inter arch static const char *intel_modern_tma_l1_events[] = { "@icl_slots", // 0x0400 (Group Leader) @@ -669,7 +629,7 @@ static const char *intel_modern_tma_l3_events[] = { // intel_modern_tma_l3 use the same compute function as skl -// === Zen arch === +// Zen arch static const char *amd_zen4_tma_l1_events[] = { "@zen4_cyc", // 0x0076 (Cycles) @@ -772,7 +732,7 @@ static void compute_amd_zen4_tma_l2(const double *raw, double *final) { } -// === ARM arch === +// ARM arch static const int arm_l1_passes[] = {5}; static const char *arm_tma_l1_events[] = { @@ -880,17 +840,46 @@ static void compute_arm_tma_l2(const double *raw, double *final) { #endif //__linux__ -// === Core logic === +// 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; -/* AMD metrics - * backend_bound_memory = Memory bound - * backend_bound_cpu = Core Bound - * frontend_bound_latency = Fetch Latency - * frontend_bound_bw = Bandwidth - * bad_speculation_mispredicts = Branch Mispredicts - */ +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]); @@ -911,181 +900,31 @@ static void compute_arm_tma_l2(const double *raw, double *final) { * */ int resolve_metric(const char *metric_name, metric_resolver_t *out_resolver) { -#ifdef __linux__ - if (strcmp(metric_name, "TopdownL1") == 0) { - - if (detect_if_intel()) { - //fprintf(stderr,"[DEBUG] INTEL detected\n"); - intel_arch_t uarch = detect_intel_microarchitecture(); - - if (uarch == INTEL_SKYLAKE_CASCADE) { - //fprintf(stderr,"[DEBUG] Old Intel detected\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 5; - out_resolver->hw_events = skl_tma_l1_events; - out_resolver->num_results = 4; - out_resolver->num_passes = 1; - out_resolver->events_per_pass = pass_5; - out_resolver->compute_formula = compute_skl_tma_l1; - return 1; - } else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - //fprintf(stderr,"[DEBUG] Modern Intel detected\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 5; - out_resolver->hw_events = intel_modern_tma_l1_events; - out_resolver->num_results = 4; - out_resolver->num_passes = 1; - out_resolver->events_per_pass = pass_5; - out_resolver->compute_formula = compute_intel_modern_tma_l1; - return 1; - } - } - else if (detect_if_amd()) { - amd_arch_t uarch = detect_amd_microarchitecture(); - - if (uarch == AMD_ZEN_4) { - //fprintf(stderr,"[DEBUG] AMD_ZEN_4 detected\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 4; - out_resolver->hw_events = amd_zen4_tma_l1_events; - out_resolver->num_results = 4; - out_resolver->num_passes = 1; - out_resolver->events_per_pass = pass_4; - out_resolver->compute_formula = compute_amd_zen34_tma_l1; - return 1; - } - else if (uarch == AMD_ZEN_1_2) { - //fprintf(stderr,"[DEBUG] AMD_ZEN_1_2 detected (unsuported)\n"); - //out_resolver->is_supported = 1; - //out_resolver->num_hw_events = 4; - //out_resolver->hw_events = amd_zen4_tma_l1_events; // Todo change to zen1 - //out_resolver->num_results = 4; - //out_resolver->num_passes = 1; - //out_resolver->events_per_pass = pass_4; - //out_resolver->compute_formula = compute_amd_zen34_tma_l1; // Todo change to zen1 - return 0; - } - // else if (uarch == AMD_ZEN_3) ... - } - else if (detect_if_arm()) { - //fprintf(stderr,"[DEBUG] ARM AArch64 detected\n"); +#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_hw_events = 5; - out_resolver->hw_events = arm_tma_l1_events; - out_resolver->num_results = 4; - out_resolver->num_passes = 1; - out_resolver->events_per_pass = arm_l1_passes; - out_resolver->compute_formula = compute_arm_tma_l1; + 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; } } - else if (strcmp(metric_name, "TopdownL2") == 0) { - if (detect_if_intel()) { // L2 intel - intel_arch_t uarch = detect_intel_microarchitecture(); - - if (uarch == INTEL_SKYLAKE_CASCADE) { - //fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L2 (Multi-Pass)\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 12; - out_resolver->hw_events = skl_tma_l2_events; - out_resolver->num_results = 8; - out_resolver->num_passes = 3; - out_resolver->events_per_pass = skl_l2_passes; // Tableau {4, 4, 4} - out_resolver->compute_formula = compute_skl_tma_l2; - return 1; - } - else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - //fprintf(stderr,"[DEBUG] INTEL_ICELAKE_SAPPHIRE L2\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 9; - out_resolver->hw_events = intel_modern_tma_l2_events; - out_resolver->num_results = 8; - out_resolver->num_passes = 1; - out_resolver->events_per_pass = pass_9; - out_resolver->compute_formula = compute_intel_modern_tma_l2; - return 1; - } - } - - else if (detect_if_amd()) { // L2 amd - amd_arch_t uarch = detect_amd_microarchitecture(); - if (uarch == AMD_ZEN_4) { - //fprintf(stderr,"[DEBUG] AMD_ZEN_4 L2 (Multi-Pass)\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 9; - out_resolver->hw_events = amd_zen4_tma_l2_events; - out_resolver->num_results = 8; - out_resolver->num_passes = 2; - out_resolver->events_per_pass = amd_zen4_l2_passes; - out_resolver->compute_formula = compute_amd_zen4_tma_l2; - return 1; - } - } - return 0; - } - else if (strcmp(metric_name, "TopdownL3") == 0) { - if (detect_if_intel()) { - intel_arch_t uarch = detect_intel_microarchitecture(); - - if (uarch == INTEL_SKYLAKE_CASCADE) { - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 40; - out_resolver->hw_events = skl_tma_l3_events; - out_resolver->num_results = 26; - out_resolver->num_passes = 8; - out_resolver->events_per_pass = skl_l3_passes; - out_resolver->compute_formula = compute_skl_tma_l3; - return 1; - } - else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 40; - out_resolver->hw_events = intel_modern_tma_l3_events; - out_resolver->num_results = 26; - out_resolver->num_passes = 8; - out_resolver->events_per_pass = icl_l3_passes; - out_resolver->compute_formula = compute_skl_tma_l3; - return 1; - } - } - return 0; - } - else if (strcmp(metric_name, "TopdownL3_Mem") == 0) { - if (detect_if_intel()) { - intel_arch_t uarch = detect_intel_microarchitecture(); - if (uarch == INTEL_SKYLAKE_CASCADE) { - //fprintf(stderr,"[DEBUG] INTEL_SKYLAKE_CASCADE L3_Mem (Multi-Pass)\n"); - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 7; - out_resolver->hw_events = skl_tma_l3_mem_events; - out_resolver->num_results = 5; - out_resolver->num_passes = 2; - out_resolver->events_per_pass = skl_l3_mem_passes; - out_resolver->compute_formula = compute_skl_tma_l3_mem; - return 1; - } - else if (uarch == INTEL_ICELAKE_SAPPHIRE) { - out_resolver->is_supported = 1; - out_resolver->num_hw_events = 7; - out_resolver->hw_events = intel_modern_tma_l3_mem_events; - out_resolver->num_results = 5; - out_resolver->num_passes = 2; - out_resolver->events_per_pass = icl_l3_mem_passes; - out_resolver->compute_formula = compute_skl_tma_l3_mem; // same as skl - return 1; - } - } - return 0; - } - // Unsuported hardware / metric or the event is a pmu + out_resolver->is_supported = 0; return 0; - #else - (void)metric_name; - (void)out_resolver; - return 0; - #endif //__linux__ - +#endif } int get_perf_metric_results_count(const char *metric_name) { diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index e1faa290..04848df0 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -171,6 +171,71 @@ def validate_outputs( } +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]], @@ -246,61 +311,16 @@ def evaluate_performance( 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'..." ) - perf_path = shutil.which("perf") - if not perf_path: - return (eval_results, 0, "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] - - # amd have no native TopdownL2 but we can reconstruct it with right metrics - is_amd = False - if sys.platform == "linux": - try: - with open("/proc/cpuinfo", "r") as f: - if "AuthenticAMD" in f.read(): - is_amd = True - except: - 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 len(perf_events) > 0: - cmd.extend(["-e", ",".join(perf_events)]) - if len(perf_metrics) > 0: - cmd.extend(["-M", ",".join(perf_metrics)]) - - try: - # print("[DEBUG] Starting perf...") - perf_proc = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) - - time.sleep(0.75) - - # Rerun evaluation without HW counters to generate activity for perf + def dummy_workload(): dummy_results = (ctypes.c_double * repeat)() if cfunc.is_packed: - # print("[DEBUG] Rerun packed (perf)...") - _ = runtime.evaluate_packed_perf( + runtime.evaluate_packed_perf( dummy_results, [], repeat, @@ -312,10 +332,7 @@ def evaluate_performance( len(args_tuples), ) else: - assert args_array_packed is not None - assert args_codes_packed is not None - # print("[DEBUG] Rerun not packed (perf)...") - args_array = (ctypes.c_voidp * len(args_tuples))( + _args_array = (ctypes.c_voidp * len(args_tuples))( *[arg[0] for arg in args_tuples] ) runtime.evaluate_perf( @@ -326,30 +343,12 @@ def evaluate_performance( min_repeat_ms, cfunc, args_array, - len(args_array), + len(args_tuples), ) - # print("[DEBUG] Stopping perf...") - 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 (eval_results, 0, formatted_fallback_output) - - except Exception as e: - # print(f"[DEBUG] Fallback perf stat failed : {e}") - return (eval_results, 0, f"Fallback perf stat failed: {e}") + fallback_output = _fallback_perf_stat(failed_counters, dummy_workload) - # Return API result - return (eval_results, 0, "") + return (eval_results, 0, fallback_output) def copy_outputs( From e2defecf9ff74bc10506d5e8420489a0f25e6d3c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 29 Jun 2026 09:43:56 +0200 Subject: [PATCH 84/92] [TMA] remove unused package --- src/xtc/utils/evaluation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xtc/utils/evaluation.py b/src/xtc/utils/evaluation.py index 04848df0..db2936fa 100644 --- a/src/xtc/utils/evaluation.py +++ b/src/xtc/utils/evaluation.py @@ -7,7 +7,6 @@ import shutil import signal import subprocess -import sys import time from typing import Any, Callable, cast From 40640690bd0d173a3674d6404e9edacf3368a951 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 29 Jun 2026 09:44:45 +0200 Subject: [PATCH 85/92] [TMA] add pyplot as optional dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2e1497ce..b6f8617f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,8 @@ dev = [ "types-PyYAML", "marimo>=0.20.0", "ruff==0.14.10", - "matplotlib" + "matplotlib", + "plotly" ] [tool.setuptools] From 0f07385623e0c83d636b26fa3f7c689ff6672d5c Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 29 Jun 2026 10:09:27 +0200 Subject: [PATCH 86/92] [TMA] Change marimo output limit --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b6f8617f..53a2b40e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,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 From 3b15c9dc888c8fef71db3da02d86435725fe869e Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 29 Jun 2026 10:10:11 +0200 Subject: [PATCH 87/92] [TMA] update HW tutorial --- docs/tutorials/hw_counters_introduction_v2.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/tutorials/hw_counters_introduction_v2.py b/docs/tutorials/hw_counters_introduction_v2.py index 1e19e2d7..a5e18636 100644 --- a/docs/tutorials/hw_counters_introduction_v2.py +++ b/docs/tutorials/hw_counters_introduction_v2.py @@ -481,27 +481,25 @@ def _(mo): I = 512 J = 512 K = 512 - # 1. Global navigation (RAM) + # Global navigation (RAM) for i_out in range(I / 2): # Steps of 2 for j_out in range(J / 16): # Steps of 16 - # --- LOAD HOISTING --- - # We load 2 independent vector registers (16 floats each) once! + # Load hosting C_vec_0 = C[i, j : j+16] C_vec_1 = C[i + 1, j : j+16] - # 2. Accumulation loop - for k in range(K): # 4096 iterations! + # Accumulation loop + for k in range(K) # Vector Load (16 floats loaded at once) B_vec = B[k, j : j+16] - # 3. Unrolled & Vectorized Micro-kernel + # Unrolled & Vectorized Micro-kernel C_vec_0 += A[i, k] * B_vec C_vec_1 += A[i + 1, k] * B_vec - # --- STORE HOISTING --- - # We trigger 2 simultaneous vector stores to memory! + # Store hoisting C[i, j : j+16] = C_vec_0 C[i + 1, j : j+16] = C_vec_1 ``` @@ -560,11 +558,6 @@ def _(mo): 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 From 6f52389e4e551c721c8a3e5f3fad46a75f6b9cc2 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 29 Jun 2026 11:19:35 +0200 Subject: [PATCH 88/92] [TMA] Fix declaration for MacOS compilation --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index 14c0e60f..dfa474e3 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -837,7 +837,6 @@ static void compute_arm_tma_l2(const double *raw, double *final) { } } -#endif //__linux__ // Resolver logic @@ -881,6 +880,7 @@ static const metric_registry_entry_t METRICS_REGISTRY[] = { }; static const int METRICS_REGISTRY_SIZE = sizeof(METRICS_REGISTRY) / sizeof(METRICS_REGISTRY[0]); +#endif //__linux__ /* From 1e4ed1643256459a74d785746a4fb0956f792272 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Mon, 29 Jun 2026 14:08:16 +0200 Subject: [PATCH 89/92] [TMA] TMAs are now correctly calculated when evaluate with packed args --- src/xtc/csrcs/runtimes/host/evaluate_perf.c | 104 +++++++++++++++----- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/evaluate_perf.c b/src/xtc/csrcs/runtimes/host/evaluate_perf.c index febd4a37..70d7f11f 100644 --- a/src/xtc/csrcs/runtimes/host/evaluate_perf.c +++ b/src/xtc/csrcs/runtimes/host/evaluate_perf.c @@ -125,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) { @@ -184,6 +184,50 @@ 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; @@ -192,22 +236,14 @@ typedef struct { int start_pass; } metric_map_t; -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) + +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) { - switch (nargs) { - case 0: evaluate0_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func0_t)func); break; - case 1: evaluate1_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; - case 2: evaluate2_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; - case 3: evaluate3_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; - case 4: evaluate4_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; - case 5: evaluate5_perf(results, 0, NULL, repeat, number, min_repeat_ms, (func5_t)func, args[0], args[1], args[2], args[3], args[4]); break; - case 6: evaluate6_perf(results, 0, NULL, 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; - } + dispatch_execution(results, 0, NULL, repeat, number, min_repeat_ms, ctx); return; } @@ -294,22 +330,15 @@ void evaluate_perf(double *results, int events_num, const char *events_names[], free(test_fds); if (all_failed) { - fprintf(stderr,"[DEBUG] execution bypassed all events failed for the pass %d\n",pass_events_num); + 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 { - switch (nargs) { - case 0: evaluate0_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func0_t)func); break; - case 1: evaluate1_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func1_t)func, args[0]); break; - case 2: evaluate2_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func2_t)func, args[0], args[1]); break; - case 3: evaluate3_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func3_t)func, args[0], args[1], args[2]); break; - case 4: evaluate4_perf(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func4_t)func, args[0], args[1], args[2], args[3]); break; - case 5: evaluate5_perf(pass_results, pass_events_num, pass_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(pass_results, pass_events_num, pass_events_names, repeat, number, min_repeat_ms, (func6_t)func, args[0], args[1], args[2], args[3], args[4], args[5]); break; - } + 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++) { @@ -351,6 +380,33 @@ void evaluate_perf(double *results, int events_num, const char *events_names[], 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) +{ + 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, int repeat, int number, int min_repeat_ms, void (*func)(), void **args, int nargs) From 18c0692bf490bc2b2b613d9b20081d7565a11b32 Mon Sep 17 00:00:00 2001 From: SUBE Lucas Date: Tue, 30 Jun 2026 12:15:21 +0200 Subject: [PATCH 90/92] [TMA] Fix : double include of cpuid.h with older gcc version --- src/xtc/csrcs/runtimes/host/perf_metrics.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/xtc/csrcs/runtimes/host/perf_metrics.c b/src/xtc/csrcs/runtimes/host/perf_metrics.c index dfa474e3..317219ca 100644 --- a/src/xtc/csrcs/runtimes/host/perf_metrics.c +++ b/src/xtc/csrcs/runtimes/host/perf_metrics.c @@ -26,9 +26,6 @@ #endif #if ARCH_IS_X86 -#include - - #define GET_METRIC(m, i) (((m) >> (i*8)) & 0xff) From 24b384e519a351365b4f8aa4a184f3bcfb577bf9 Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Fri, 29 May 2026 15:35:18 +0200 Subject: [PATCH 91/92] [DESCRIPT] Cumulative perf graph [DESCRIPT] Edit marimo version for matplotlib support --- docs/tutorials/descript++_notebook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py index f9bba070..0d2ef37f 100644 --- a/docs/tutorials/descript++_notebook.py +++ b/docs/tutorials/descript++_notebook.py @@ -567,10 +567,10 @@ def get_info(): j#j1: vectorize=j_v constraints: - j1+j1*i1<128 - - i1*j1 > 8 + - i1*j1 > 16 """ strategy = Strategy(graph, schedule_spec, partial_unrolls=False, partial_tiles=False) - backend = TVM_Backend + backend = MLIR_Backend configurations = list(strategy.sample(1000)) total = len(configurations) From 88411d63025bb0f8edab06959686016583ace7b5 Mon Sep 17 00:00:00 2001 From: Leon Frenot Date: Fri, 3 Jul 2026 11:41:29 +0200 Subject: [PATCH 92/92] [DESCRIPT] Adding paragraphs for Tiling and Splitting --- docs/tutorials/descript++_notebook.py | 119 +++++++++++++++++--------- 1 file changed, 78 insertions(+), 41 deletions(-) diff --git a/docs/tutorials/descript++_notebook.py b/docs/tutorials/descript++_notebook.py index 0d2ef37f..c4e18647 100644 --- a/docs/tutorials/descript++_notebook.py +++ b/docs/tutorials/descript++_notebook.py @@ -265,7 +265,7 @@ def _(mo): mo.md(r""" # Descript - XTC allows you to descripte 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. + 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 @@ -298,34 +298,6 @@ def _(mo): ``` 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. - - `descript` also has syntax for loop spliting. - - For example: - ``` - 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 @@ -457,7 +429,7 @@ def _(mo): @app.cell(hide_code=True) def _(mo): mo.md(r""" - `descript` can also be used to describe loop structures without specifying every tile size. This allows you to easily write a set of strategies to explore. + `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 @@ -466,8 +438,8 @@ def _(mo): k: j#16: k#32: - i#i1: unroll - j#j1: vectorize + i#i1: unroll=i_r + j#j1: vectorize=j_v ``` leaves the sizes of the inner kernel as variables to explore. @@ -481,13 +453,7 @@ def _(mo): @app.cell(hide_code=True) def _(mo): mo.md(r""" - **Practice.** The code below defines several schedule configurations using our declarative scheduler. You can: - - - **Add/modify configurations**: Try different tile sizes, loop orderings, or optimization combinations. The pre-built search space (variable `configurations` in function `explore` and lines above) may be poorly designed... - - **Change the acquisition function**: Add caching, error handling, or custom metrics - - **Modify the exploration loop**: Add early stopping or custom filtering - - The `explore()` function must `yield` tuples of `(index, total, config_name, performance)` for real-time progress display. + **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 @@ -662,6 +628,20 @@ def _( 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""" @@ -674,6 +654,63 @@ def _(mo): 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""" @@ -681,7 +718,7 @@ def _(mo): `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, but this can be changed by adding `partial_unrolls=True` to the strategy's arguments. + 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 | |-------------------------------------|-------------------------------------------------------------------| @@ -709,7 +746,7 @@ def _(mo): `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. + 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 | |-------------------------------------|-------------------------------------------------------------------|