mrm-trace 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mrm_trace/__init__.py +7 -0
- mrm_trace/analyser/__init__.py +53 -0
- mrm_trace/analyser/iai.py +67 -0
- mrm_trace/analyser/locality.py +83 -0
- mrm_trace/analyser/read_freq.py +40 -0
- mrm_trace/analyser/retention.py +83 -0
- mrm_trace/analyser/suitability.py +63 -0
- mrm_trace/analyser/working_set.py +49 -0
- mrm_trace/analyser/write_once.py +50 -0
- mrm_trace/api.py +52 -0
- mrm_trace/cli.py +140 -0
- mrm_trace/collector/__init__.py +42 -0
- mrm_trace/collector/artifact_manager.py +67 -0
- mrm_trace/collector/base.py +46 -0
- mrm_trace/collector/memray_runner.py +83 -0
- mrm_trace/collector/perf_runner.py +164 -0
- mrm_trace/collector/process_monitor.py +115 -0
- mrm_trace/config/__init__.py +4 -0
- mrm_trace/config/loader.py +28 -0
- mrm_trace/config/schema.py +220 -0
- mrm_trace/config/validators.py +143 -0
- mrm_trace/engines/__init__.py +29 -0
- mrm_trace/engines/base.py +140 -0
- mrm_trace/engines/llamacpp.py +200 -0
- mrm_trace/engines/vllm.py +176 -0
- mrm_trace/labeller/__init__.py +15 -0
- mrm_trace/labeller/address_tracker.py +111 -0
- mrm_trace/labeller/kv_lifecycle.py +80 -0
- mrm_trace/labeller/labeller.py +228 -0
- mrm_trace/labeller/symbol_rules.py +106 -0
- mrm_trace/orchestration/__init__.py +0 -0
- mrm_trace/parser/__init__.py +13 -0
- mrm_trace/parser/memray_parser.py +93 -0
- mrm_trace/parser/normalizer.py +61 -0
- mrm_trace/parser/perf_script_parser.py +102 -0
- mrm_trace/parser/schema.py +33 -0
- mrm_trace/parser/writer.py +54 -0
- mrm_trace/reporter/__init__.py +24 -0
- mrm_trace/reporter/figures.py +138 -0
- mrm_trace/reporter/manifest.py +59 -0
- mrm_trace/reporter/metrics_csv.py +87 -0
- mrm_trace/reporter/parquet_export.py +52 -0
- mrm_trace/reporter/run_exporter.py +123 -0
- mrm_trace/schema_version.py +127 -0
- mrm_trace/telemetry/__init__.py +21 -0
- mrm_trace/telemetry/baseline.py +68 -0
- mrm_trace/telemetry/observer_effect.py +66 -0
- mrm_trace/telemetry/thermal.py +77 -0
- mrm_trace/telemetry/validity.py +128 -0
- mrm_trace/utils/__init__.py +0 -0
- mrm_trace/utils/files.py +18 -0
- mrm_trace/utils/ids.py +17 -0
- mrm_trace/utils/logging.py +48 -0
- mrm_trace-0.1.0.dist-info/METADATA +234 -0
- mrm_trace-0.1.0.dist-info/RECORD +58 -0
- mrm_trace-0.1.0.dist-info/WHEEL +5 -0
- mrm_trace-0.1.0.dist-info/entry_points.txt +2 -0
- mrm_trace-0.1.0.dist-info/top_level.txt +1 -0
mrm_trace/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Analysis subsystem for mrm-trace — Phase 6."""
|
|
2
|
+
|
|
3
|
+
from mrm_trace.analyser.iai import compute_iai
|
|
4
|
+
from mrm_trace.analyser.locality import compute_locality
|
|
5
|
+
from mrm_trace.analyser.read_freq import compute_read_freq
|
|
6
|
+
from mrm_trace.analyser.retention import compute_retention
|
|
7
|
+
from mrm_trace.analyser.suitability import classify_suitability
|
|
8
|
+
from mrm_trace.analyser.working_set import compute_working_set
|
|
9
|
+
from mrm_trace.analyser.write_once import compute_write_once
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"compute_retention",
|
|
13
|
+
"compute_write_once",
|
|
14
|
+
"compute_read_freq",
|
|
15
|
+
"compute_working_set",
|
|
16
|
+
"compute_locality",
|
|
17
|
+
"compute_iai",
|
|
18
|
+
"classify_suitability",
|
|
19
|
+
"compute_all",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compute_all(trace) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Run all analysis modules on a trace DataFrame.
|
|
26
|
+
|
|
27
|
+
Returns a dict with keys:
|
|
28
|
+
retention_per_region, retention_summary,
|
|
29
|
+
write_once, read_freq,
|
|
30
|
+
working_set_per_region, working_set_summary,
|
|
31
|
+
locality_per_region, locality_summary,
|
|
32
|
+
iai, suitability
|
|
33
|
+
"""
|
|
34
|
+
ret_per_region, ret_summary = compute_retention(trace)
|
|
35
|
+
wo = compute_write_once(trace)
|
|
36
|
+
rf = compute_read_freq(trace)
|
|
37
|
+
ws_per_region, ws_summary = compute_working_set(trace)
|
|
38
|
+
loc_per_region, loc_summary = compute_locality(trace)
|
|
39
|
+
iai = compute_iai(trace)
|
|
40
|
+
suit = classify_suitability(ret_summary, wo, rf)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"retention_per_region": ret_per_region,
|
|
44
|
+
"retention_summary": ret_summary,
|
|
45
|
+
"write_once": wo,
|
|
46
|
+
"read_freq": rf,
|
|
47
|
+
"working_set_per_region": ws_per_region,
|
|
48
|
+
"working_set_summary": ws_summary,
|
|
49
|
+
"locality_per_region": loc_per_region,
|
|
50
|
+
"locality_summary": loc_summary,
|
|
51
|
+
"iai": iai,
|
|
52
|
+
"suitability": suit,
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inter-access interval (IAI) analysis — time between consecutive accesses.
|
|
3
|
+
|
|
4
|
+
Groups by exact address (not address_page) to correctly handle sub-page KV
|
|
5
|
+
blocks at 256-byte stride where multiple blocks share a 4K page.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compute_iai(trace: pd.DataFrame) -> pd.DataFrame:
|
|
13
|
+
"""
|
|
14
|
+
Compute inter-access interval distribution per region_type.
|
|
15
|
+
|
|
16
|
+
IAI = time gap between consecutive accesses to the *same exact address*,
|
|
17
|
+
sorted by timestamp_ns within each address group.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
DataFrame
|
|
22
|
+
Columns: region_type, n_intervals, iai_p50_ns, iai_p90_ns,
|
|
23
|
+
iai_p99_ns, iai_mean_ns
|
|
24
|
+
One row per region_type (regions with < 2 accesses to any address
|
|
25
|
+
contribute 0 intervals and are excluded from the summary if they
|
|
26
|
+
have no intervals at all).
|
|
27
|
+
"""
|
|
28
|
+
trace_sorted = trace.sort_values(["address", "timestamp_ns"])
|
|
29
|
+
all_intervals = []
|
|
30
|
+
|
|
31
|
+
for (address, region_type), grp in trace_sorted.groupby(
|
|
32
|
+
["address", "region_type"], sort=False
|
|
33
|
+
):
|
|
34
|
+
ts = grp["timestamp_ns"].to_numpy(dtype=np.int64)
|
|
35
|
+
if len(ts) < 2:
|
|
36
|
+
continue
|
|
37
|
+
intervals = np.diff(ts)
|
|
38
|
+
intervals = intervals[intervals > 0] # ignore same-timestamp duplicates
|
|
39
|
+
if len(intervals) == 0:
|
|
40
|
+
continue
|
|
41
|
+
all_intervals.append(
|
|
42
|
+
pd.DataFrame({"region_type": region_type, "iai_ns": intervals})
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if not all_intervals:
|
|
46
|
+
return pd.DataFrame(
|
|
47
|
+
columns=["region_type", "n_intervals", "iai_p50_ns",
|
|
48
|
+
"iai_p90_ns", "iai_p99_ns", "iai_mean_ns"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
combined = pd.concat(all_intervals, ignore_index=True)
|
|
52
|
+
|
|
53
|
+
rows = []
|
|
54
|
+
for rtype, grp in combined.groupby("region_type"):
|
|
55
|
+
s = grp["iai_ns"]
|
|
56
|
+
rows.append(
|
|
57
|
+
{
|
|
58
|
+
"region_type": rtype,
|
|
59
|
+
"n_intervals": len(s),
|
|
60
|
+
"iai_p50_ns": int(s.quantile(0.50)),
|
|
61
|
+
"iai_p90_ns": int(s.quantile(0.90)),
|
|
62
|
+
"iai_p99_ns": int(s.quantile(0.99)),
|
|
63
|
+
"iai_mean_ns": float(s.mean()),
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return pd.DataFrame(rows)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Access locality analysis — stride distribution and same-page access fraction.
|
|
3
|
+
|
|
4
|
+
Locality measures spatial regularity: small strides and high same-page fraction
|
|
5
|
+
indicate cache-friendly, predictable access patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_locality(trace: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
15
|
+
"""
|
|
16
|
+
Compute locality statistics per region.
|
|
17
|
+
|
|
18
|
+
Stride = |address[i+1] - address[i]| within consecutive accesses to same region.
|
|
19
|
+
Same-page fraction = fraction of consecutive accesses that stay on the same page.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
per_region : DataFrame
|
|
24
|
+
Columns: region_id, region_type, n_accesses, mean_stride_bytes,
|
|
25
|
+
median_stride_bytes, same_page_fraction
|
|
26
|
+
type_summary : DataFrame
|
|
27
|
+
Columns: region_type, mean_stride_bytes, median_stride_bytes,
|
|
28
|
+
same_page_fraction
|
|
29
|
+
"""
|
|
30
|
+
rows = []
|
|
31
|
+
trace_sorted = trace.sort_values(["region_id", "timestamp_ns"])
|
|
32
|
+
|
|
33
|
+
for region_id, grp in trace_sorted.groupby("region_id"):
|
|
34
|
+
region_type = grp["region_type"].iloc[0]
|
|
35
|
+
n = len(grp)
|
|
36
|
+
|
|
37
|
+
if n < 2:
|
|
38
|
+
rows.append(
|
|
39
|
+
{
|
|
40
|
+
"region_id": region_id,
|
|
41
|
+
"region_type": region_type,
|
|
42
|
+
"n_accesses": n,
|
|
43
|
+
"mean_stride_bytes": 0.0,
|
|
44
|
+
"median_stride_bytes": 0.0,
|
|
45
|
+
"same_page_fraction": 0.0,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
addrs = grp["address"].to_numpy()
|
|
51
|
+
pages = grp["address_page"].to_numpy()
|
|
52
|
+
strides = np.abs(np.diff(addrs.astype(np.int64)))
|
|
53
|
+
same_page = np.sum(pages[1:] == pages[:-1])
|
|
54
|
+
|
|
55
|
+
rows.append(
|
|
56
|
+
{
|
|
57
|
+
"region_id": region_id,
|
|
58
|
+
"region_type": region_type,
|
|
59
|
+
"n_accesses": n,
|
|
60
|
+
"mean_stride_bytes": float(strides.mean()),
|
|
61
|
+
"median_stride_bytes": float(np.median(strides)),
|
|
62
|
+
"same_page_fraction": float(same_page / len(strides)),
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
per_region = pd.DataFrame(rows)
|
|
67
|
+
if per_region.empty:
|
|
68
|
+
return per_region, pd.DataFrame(
|
|
69
|
+
columns=["region_type", "mean_stride_bytes", "median_stride_bytes",
|
|
70
|
+
"same_page_fraction"]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
type_summary = (
|
|
74
|
+
per_region.groupby("region_type")
|
|
75
|
+
.agg(
|
|
76
|
+
mean_stride_bytes=("mean_stride_bytes", "mean"),
|
|
77
|
+
median_stride_bytes=("median_stride_bytes", "mean"),
|
|
78
|
+
same_page_fraction=("same_page_fraction", "mean"),
|
|
79
|
+
)
|
|
80
|
+
.reset_index()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return per_region, type_summary
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read frequency analysis — total accesses, read fraction, reads-per-write.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_read_freq(trace: pd.DataFrame) -> pd.DataFrame:
|
|
9
|
+
"""
|
|
10
|
+
Compute read frequency statistics per region.
|
|
11
|
+
|
|
12
|
+
Returns
|
|
13
|
+
-------
|
|
14
|
+
DataFrame
|
|
15
|
+
Columns: region_id, region_type, total_reads, total_writes,
|
|
16
|
+
total_accesses, read_fraction, reads_per_write
|
|
17
|
+
"""
|
|
18
|
+
region_types = trace.groupby("region_id")["region_type"].first()
|
|
19
|
+
|
|
20
|
+
counts = trace.groupby(["region_id", "op_type"]).size().unstack(fill_value=0)
|
|
21
|
+
|
|
22
|
+
# Ensure both columns exist even if all ops are one type
|
|
23
|
+
for col in ("load", "store"):
|
|
24
|
+
if col not in counts.columns:
|
|
25
|
+
counts[col] = 0
|
|
26
|
+
|
|
27
|
+
counts = counts.rename(columns={"load": "total_reads", "store": "total_writes"})
|
|
28
|
+
counts["total_accesses"] = counts["total_reads"] + counts["total_writes"]
|
|
29
|
+
counts["read_fraction"] = (counts["total_reads"] / counts["total_accesses"]).fillna(0.0)
|
|
30
|
+
counts["reads_per_write"] = (
|
|
31
|
+
counts["total_reads"] / counts["total_writes"].replace(0, float("nan"))
|
|
32
|
+
).fillna(0.0)
|
|
33
|
+
|
|
34
|
+
result = counts.reset_index()
|
|
35
|
+
result["region_type"] = result["region_id"].map(region_types)
|
|
36
|
+
|
|
37
|
+
return result[
|
|
38
|
+
["region_id", "region_type", "total_reads", "total_writes",
|
|
39
|
+
"total_accesses", "read_fraction", "reads_per_write"]
|
|
40
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retention analysis — how long each region is live between first write and last read.
|
|
3
|
+
|
|
4
|
+
Per-region: first_write_ns, last_read_ns, retention_ns, retention_s
|
|
5
|
+
Summary: per region_type percentile stats (p50, p90, p99, mean)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def compute_retention(trace: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
14
|
+
"""
|
|
15
|
+
Compute retention duration for each region.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
per_region : DataFrame
|
|
20
|
+
Columns: region_id, region_type, first_write_ns, last_read_ns,
|
|
21
|
+
retention_ns, retention_s
|
|
22
|
+
summary : DataFrame
|
|
23
|
+
Columns: region_type, n_regions, retention_p50_s, retention_p90_s,
|
|
24
|
+
retention_p99_s, retention_mean_s
|
|
25
|
+
"""
|
|
26
|
+
stores = trace[trace["op_type"] == "store"]
|
|
27
|
+
loads = trace[trace["op_type"] == "load"]
|
|
28
|
+
|
|
29
|
+
first_write = (
|
|
30
|
+
stores.groupby("region_id")["timestamp_ns"].min().rename("first_write_ns")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
last_read = (
|
|
34
|
+
loads.groupby("region_id")["timestamp_ns"].max().rename("last_read_ns")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
region_types = trace.groupby("region_id")["region_type"].first()
|
|
38
|
+
|
|
39
|
+
per_region = pd.concat([region_types, first_write, last_read], axis=1).reset_index()
|
|
40
|
+
per_region.columns = ["region_id", "region_type", "first_write_ns", "last_read_ns"]
|
|
41
|
+
|
|
42
|
+
# Regions that were never written — use first access as write time
|
|
43
|
+
no_write_mask = per_region["first_write_ns"].isna()
|
|
44
|
+
if no_write_mask.any():
|
|
45
|
+
fallback = (
|
|
46
|
+
trace.groupby("region_id")["timestamp_ns"].min().rename("first_write_ns")
|
|
47
|
+
)
|
|
48
|
+
per_region.loc[no_write_mask, "first_write_ns"] = per_region.loc[
|
|
49
|
+
no_write_mask, "region_id"
|
|
50
|
+
].map(fallback)
|
|
51
|
+
|
|
52
|
+
# Regions with no reads — retention = 0
|
|
53
|
+
per_region["last_read_ns"] = per_region["last_read_ns"].fillna(
|
|
54
|
+
per_region["first_write_ns"]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
per_region["retention_ns"] = (
|
|
58
|
+
per_region["last_read_ns"] - per_region["first_write_ns"]
|
|
59
|
+
).clip(lower=0).astype("int64")
|
|
60
|
+
per_region["retention_s"] = per_region["retention_ns"] / 1e9
|
|
61
|
+
|
|
62
|
+
per_region = per_region.astype({"first_write_ns": "int64", "last_read_ns": "int64"})
|
|
63
|
+
|
|
64
|
+
# Summary per region_type
|
|
65
|
+
def _pct(g: pd.Series, q: float) -> float:
|
|
66
|
+
return float(g.quantile(q))
|
|
67
|
+
|
|
68
|
+
summary_rows = []
|
|
69
|
+
for rtype, grp in per_region.groupby("region_type"):
|
|
70
|
+
s = grp["retention_s"]
|
|
71
|
+
summary_rows.append(
|
|
72
|
+
{
|
|
73
|
+
"region_type": rtype,
|
|
74
|
+
"n_regions": len(grp),
|
|
75
|
+
"retention_p50_s": _pct(s, 0.50),
|
|
76
|
+
"retention_p90_s": _pct(s, 0.90),
|
|
77
|
+
"retention_p99_s": _pct(s, 0.99),
|
|
78
|
+
"retention_mean_s": float(s.mean()),
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
summary = pd.DataFrame(summary_rows)
|
|
82
|
+
|
|
83
|
+
return per_region, summary
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MRM suitability classification.
|
|
3
|
+
|
|
4
|
+
Thresholds
|
|
5
|
+
----------
|
|
6
|
+
high_mrm : write_once_ratio >= 0.8 AND retention_p99_s >= 10.0
|
|
7
|
+
medium_mrm : write_once_ratio >= 0.5 AND retention_p50_s >= 1.0
|
|
8
|
+
low_mrm : everything else
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def classify_suitability(
|
|
15
|
+
retention_summary: pd.DataFrame,
|
|
16
|
+
write_once_per_region: pd.DataFrame,
|
|
17
|
+
read_freq_per_region: pd.DataFrame,
|
|
18
|
+
retention_threshold_high_s: float = 10.0,
|
|
19
|
+
retention_threshold_medium_s: float = 1.0,
|
|
20
|
+
write_once_threshold_high: float = 0.8,
|
|
21
|
+
write_once_threshold_medium: float = 0.5,
|
|
22
|
+
) -> pd.DataFrame:
|
|
23
|
+
"""
|
|
24
|
+
Classify each region_type as high_mrm, medium_mrm, or low_mrm.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
retention_summary : output of compute_retention()[1]
|
|
29
|
+
write_once_per_region : output of compute_write_once()
|
|
30
|
+
read_freq_per_region : output of compute_read_freq()
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
DataFrame
|
|
35
|
+
Columns: region_type, write_once_ratio_mean, retention_p99_s,
|
|
36
|
+
retention_p50_s, suitability
|
|
37
|
+
"""
|
|
38
|
+
wo_by_type = (
|
|
39
|
+
write_once_per_region.groupby("region_type")["write_once_ratio"]
|
|
40
|
+
.mean()
|
|
41
|
+
.rename("write_once_ratio_mean")
|
|
42
|
+
.reset_index()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
merged = retention_summary.merge(wo_by_type, on="region_type", how="left")
|
|
46
|
+
merged["write_once_ratio_mean"] = merged["write_once_ratio_mean"].fillna(0.0)
|
|
47
|
+
|
|
48
|
+
def _classify(row: pd.Series) -> str:
|
|
49
|
+
wo = row["write_once_ratio_mean"]
|
|
50
|
+
p99 = row["retention_p99_s"]
|
|
51
|
+
p50 = row["retention_p50_s"]
|
|
52
|
+
if wo >= write_once_threshold_high and p99 >= retention_threshold_high_s:
|
|
53
|
+
return "high_mrm"
|
|
54
|
+
if wo >= write_once_threshold_medium and p50 >= retention_threshold_medium_s:
|
|
55
|
+
return "medium_mrm"
|
|
56
|
+
return "low_mrm"
|
|
57
|
+
|
|
58
|
+
merged["suitability"] = merged.apply(_classify, axis=1)
|
|
59
|
+
|
|
60
|
+
return merged[
|
|
61
|
+
["region_type", "write_once_ratio_mean", "retention_p99_s",
|
|
62
|
+
"retention_p50_s", "suitability"]
|
|
63
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Working set size analysis — unique pages and bytes touched per region.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
_PAGE_BYTES = 4096
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compute_working_set(trace: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
13
|
+
"""
|
|
14
|
+
Compute working set size per region and a summary per region_type.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
per_region : DataFrame
|
|
19
|
+
Columns: region_id, region_type, unique_pages, working_set_bytes
|
|
20
|
+
type_summary : DataFrame
|
|
21
|
+
Columns: region_type, n_regions, total_pages, total_bytes,
|
|
22
|
+
mean_pages_per_region, max_pages_per_region
|
|
23
|
+
"""
|
|
24
|
+
region_types = trace.groupby("region_id")["region_type"].first()
|
|
25
|
+
|
|
26
|
+
per_region = (
|
|
27
|
+
trace.groupby("region_id")["address_page"]
|
|
28
|
+
.nunique()
|
|
29
|
+
.rename("unique_pages")
|
|
30
|
+
.reset_index()
|
|
31
|
+
)
|
|
32
|
+
per_region["region_type"] = per_region["region_id"].map(region_types)
|
|
33
|
+
per_region["working_set_bytes"] = per_region["unique_pages"] * _PAGE_BYTES
|
|
34
|
+
|
|
35
|
+
type_summary_rows = []
|
|
36
|
+
for rtype, grp in per_region.groupby("region_type"):
|
|
37
|
+
type_summary_rows.append(
|
|
38
|
+
{
|
|
39
|
+
"region_type": rtype,
|
|
40
|
+
"n_regions": len(grp),
|
|
41
|
+
"total_pages": int(grp["unique_pages"].sum()),
|
|
42
|
+
"total_bytes": int(grp["working_set_bytes"].sum()),
|
|
43
|
+
"mean_pages_per_region": float(grp["unique_pages"].mean()),
|
|
44
|
+
"max_pages_per_region": int(grp["unique_pages"].max()),
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
type_summary = pd.DataFrame(type_summary_rows)
|
|
48
|
+
|
|
49
|
+
return per_region, type_summary
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Write-once analysis — what fraction of written addresses are written exactly once.
|
|
3
|
+
|
|
4
|
+
High write-once ratio indicates write-once memory behaviour ideal for MRM.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_write_once(trace: pd.DataFrame) -> pd.DataFrame:
|
|
11
|
+
"""
|
|
12
|
+
Compute write-once ratio per region.
|
|
13
|
+
|
|
14
|
+
An address is "write-once" if it appears in exactly one store access.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
DataFrame
|
|
19
|
+
Columns: region_id, region_type, n_written_addresses,
|
|
20
|
+
n_write_once_addresses, write_once_ratio
|
|
21
|
+
"""
|
|
22
|
+
stores = trace[trace["op_type"] == "store"].copy()
|
|
23
|
+
|
|
24
|
+
if stores.empty:
|
|
25
|
+
return pd.DataFrame(
|
|
26
|
+
columns=[
|
|
27
|
+
"region_id", "region_type", "n_written_addresses",
|
|
28
|
+
"n_write_once_addresses", "write_once_ratio",
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
write_counts = (
|
|
33
|
+
stores.groupby(["region_id", "address"]).size().reset_index(name="write_count")
|
|
34
|
+
)
|
|
35
|
+
region_types = trace.groupby("region_id")["region_type"].first().reset_index()
|
|
36
|
+
|
|
37
|
+
per_region = write_counts.groupby("region_id").agg(
|
|
38
|
+
n_written_addresses=("address", "count"),
|
|
39
|
+
n_write_once_addresses=("write_count", lambda x: (x == 1).sum()),
|
|
40
|
+
).reset_index()
|
|
41
|
+
|
|
42
|
+
per_region = per_region.merge(region_types, on="region_id", how="left")
|
|
43
|
+
per_region["write_once_ratio"] = (
|
|
44
|
+
per_region["n_write_once_addresses"] / per_region["n_written_addresses"]
|
|
45
|
+
).clip(0.0, 1.0)
|
|
46
|
+
|
|
47
|
+
return per_region[
|
|
48
|
+
["region_id", "region_type", "n_written_addresses",
|
|
49
|
+
"n_write_once_addresses", "write_once_ratio"]
|
|
50
|
+
]
|
mrm_trace/api.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
from mrm_trace.config.loader import load_experiment
|
|
5
|
+
from mrm_trace.config.schema import ExperimentConfig, RunConfig
|
|
6
|
+
from mrm_trace.config.validators import (
|
|
7
|
+
expand_sweep,
|
|
8
|
+
validate_config_semantics,
|
|
9
|
+
validate_environment,
|
|
10
|
+
validate_model_paths,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Experiment:
|
|
15
|
+
"""Python API for defining and executing mrm-trace experiments."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: ExperimentConfig) -> None:
|
|
18
|
+
self._config = config
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_yaml(cls, path: Union[str, Path]) -> "Experiment":
|
|
22
|
+
"""Load an experiment from a YAML config file."""
|
|
23
|
+
return cls(load_experiment(path))
|
|
24
|
+
|
|
25
|
+
def validate(self) -> bool:
|
|
26
|
+
"""Validate config and environment. Returns True if no errors."""
|
|
27
|
+
all_ok = True
|
|
28
|
+
for check_fn in (validate_config_semantics, validate_model_paths, validate_environment):
|
|
29
|
+
result = check_fn(self._config)
|
|
30
|
+
if not result.is_valid:
|
|
31
|
+
all_ok = False
|
|
32
|
+
return all_ok
|
|
33
|
+
|
|
34
|
+
def plan(self) -> List[RunConfig]:
|
|
35
|
+
"""Materialise and return the full list of runs for this experiment."""
|
|
36
|
+
return expand_sweep(self._config)
|
|
37
|
+
|
|
38
|
+
def run(self) -> None:
|
|
39
|
+
"""Execute all planned runs. Implemented in Phase 3."""
|
|
40
|
+
raise NotImplementedError("run() is available from Phase 3 onwards")
|
|
41
|
+
|
|
42
|
+
def analyse(self) -> None:
|
|
43
|
+
"""Parse, label, and compute metrics. Implemented in Phase 4–6."""
|
|
44
|
+
raise NotImplementedError("analyse() is available from Phase 4 onwards")
|
|
45
|
+
|
|
46
|
+
def report(self, output_dir: Union[str, Path] = "reports/") -> None:
|
|
47
|
+
"""Generate aggregated outputs and figures. Implemented in Phase 8."""
|
|
48
|
+
raise NotImplementedError("report() is available from Phase 8 onwards")
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def config(self) -> ExperimentConfig:
|
|
52
|
+
return self._config
|