rykit 0.1.0__tar.gz

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.
rykit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.3
2
+ Name: rykit
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Ryan Kosta
6
+ Author-email: Ryan Kosta <rkosta@ucsd.edu>
7
+ License: MIT
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
rykit-0.1.0/README.md ADDED
File without changes
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "rykit"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ryan Kosta", email = "rkosta@ucsd.edu" }
8
+ ]
9
+ license = { text = "MIT" }
10
+ requires-python = ">=3.13"
11
+ dependencies = []
12
+
13
+ [tool.uv]
14
+ packages = ["rykit"]
15
+ src = "src"
16
+
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.18,<0.10.0"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("Hello from rykit!")
@@ -0,0 +1,48 @@
1
+ import subprocess
2
+ def run_command_read_stderr(cmd: str) -> str:
3
+ """
4
+ Run a shell command and capture stderr output.
5
+
6
+ Args:
7
+ cmd (str): The shell command to execute.
8
+
9
+ Returns:
10
+ str: The stderr output of the command (perf writes stats here).
11
+ """
12
+ print(f"running cmd: {cmd}")
13
+ result = subprocess.run(
14
+ cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
15
+ )
16
+ output: str = result.stderr # perf outputs stats to stderr
17
+ if result.returncode == 124: # 124 is timeout exit code
18
+ print("Command timed out as expected.")
19
+ elif result.returncode != 0:
20
+ # output is stderr
21
+ print(output)
22
+ raise ValueError(f"Command failed with exit code {result.returncode}.")
23
+ else:
24
+ print("command returned 0")
25
+ return output
26
+
27
+
28
+ def run_command_read_stdout(cmd: str) -> str:
29
+ """
30
+ Run a shell command and capture stdout output.
31
+
32
+ Args:
33
+ cmd (str): The shell command to execute.
34
+
35
+ Returns:
36
+ str: The stderr output of the command (perf writes stats here).
37
+ """
38
+ print(f"running cmd: {cmd}")
39
+ result = subprocess.run(
40
+ cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
41
+ )
42
+ output: str = result.stdout
43
+ if result.returncode == 124: # 124 is timeout exit code
44
+ print("Command timed out as expected.")
45
+ elif result.returncode != 0:
46
+ print(result.stderr)
47
+ raise ValueError(f"Command failed with exit code {result.returncode}.")
48
+ return output
@@ -0,0 +1,10 @@
1
+ import glob
2
+ def get_cha_count() -> int:
3
+ """
4
+ Return the number of CHA devices under /sys/devices.
5
+
6
+ Returns:
7
+ int: Count of directories matching 'uncore_cha_*'.
8
+ """
9
+ cha_paths = glob.glob("/sys/devices/uncore_cha_*")
10
+ return len(cha_paths)
@@ -0,0 +1,108 @@
1
+ from typing import Dict,List
2
+ from rykit.cmd import run_command_read_stdout
3
+ def lscpu() -> Dict[str, str]:
4
+ """
5
+ Parse the output of `lscpu` into a dictionary.
6
+
7
+ Returns:
8
+ Dict[str, str]: Mapping of lscpu fields to their values.
9
+ """
10
+ res = {}
11
+ for line in run_command_read_stdout("lscpu").split("\n"):
12
+ if ":" not in line:
13
+ continue
14
+ segments = [x.strip() for x in line.split(":")]
15
+ res[segments[0]] = segments[1]
16
+ return res
17
+
18
+
19
+ def normalize(x: str, units: Dict[str, int], default: str):
20
+ """
21
+ Normalize a string containing a number and a unit into the default unit.
22
+
23
+ Args:
24
+ x (str): Input string, e.g., "64KB".
25
+ units (Dict[str, int]): Dictionary mapping unit strings to their scale factors.
26
+ default (str): The default unit to normalize to.
27
+
28
+ Returns:
29
+ int: The value converted to the default unit.
30
+
31
+ Raises:
32
+ AssertionError: If the default unit is not in the units dictionary.
33
+ ValueError: If the input string does not contain a recognized unit.
34
+ """
35
+ assert default in units
36
+ units_longest_first = sorted(list(units.keys()), key=len, reverse=True)
37
+ for unit in units_longest_first:
38
+ if unit in x:
39
+ val = int(x.split(unit)[0].strip())
40
+ scale_factor = units[unit] / units[default]
41
+ return int(val * scale_factor)
42
+ raise ValueError(
43
+ f"{x} did not contain a valid unit out of choices {units_longest_first}"
44
+ )
45
+
46
+
47
+ def lscpu_cache() -> Dict[str, Dict[str, str]]:
48
+ """
49
+ Parse `lscpu -C` output to get per-CPU cache and CPU info.
50
+
51
+ Returns:
52
+ Dict[str, Dict[str, str]]: Mapping from CPU ID to its properties.
53
+ """
54
+ rows = run_command_read_stdout("lscpu -C").split("\n")
55
+ col_names = rows[0].split()
56
+ res = {}
57
+ for row in rows[1:]:
58
+ cells = row.split()
59
+ if len(cells) < 1:
60
+ continue
61
+ key = cells[0]
62
+ val = {k: v for k, v in zip(col_names[1:], cells[1:])}
63
+ res[key] = val
64
+ return res
65
+
66
+
67
+ def parse_range_list(s: str) -> List[int]:
68
+ """
69
+ Convert a string representing ranges into a list of integers.
70
+
71
+ Args:
72
+ s (str): Range string, e.g., "0-3,5,7-8".
73
+
74
+ Returns:
75
+ List[int]: Expanded list of integers from the range string.
76
+ """
77
+ result = []
78
+ for part in s.split(","):
79
+ if "-" in part:
80
+ start, end = map(int, part.split("-"))
81
+ result.extend(range(start, end + 1))
82
+ else:
83
+ result.append(int(part))
84
+ return result
85
+
86
+
87
+ def get_socket(skt: int) -> List[int]:
88
+ """
89
+ Get the list of CPU IDs belonging to a specific NUMA socket.
90
+
91
+ Args:
92
+ skt (int): NUMA socket index (0-based).
93
+
94
+ Returns:
95
+ List[int]: List of CPU IDs for the socket.
96
+
97
+ Raises:
98
+ AssertionError: If the socket index is invalid.
99
+ """
100
+ assert skt >= 0
101
+
102
+ info = lscpu()
103
+ node_ct = int(info["NUMA node(s)"])
104
+
105
+ assert skt < node_ct
106
+ nodestr = info[f"NUMA node{skt} CPU(s)"]
107
+
108
+ return parse_range_list(nodestr)
@@ -0,0 +1,172 @@
1
+ from rykit.cmd import run_command_read_stdout
2
+ from rykit.cmd import run_command_read_stderr
3
+ from typing import List, Dict
4
+
5
+ def set_perf_event_paranoid(level: int):
6
+ """
7
+ Sets the kernel's perf_event_paranoid level.
8
+
9
+ The perf_event_paranoid setting controls the restrictions on
10
+ performance monitoring for non-root users:
11
+ -1 : No restrictions
12
+ 0 : Normal access, but system-wide tracepoints may be restricted
13
+ 1 : Restricted access to CPU performance events
14
+ 2 : Disallow CPU performance events for unprivileged users
15
+ 3 : Maximum restriction (default on many systems)
16
+
17
+ Args:
18
+ level (int): The desired paranoid level (-1 to 3).
19
+
20
+ Raises:
21
+ AssertionError: If level is not an int or not in the allowed range.
22
+ """
23
+ assert type(level) == int
24
+ assert -1 <= level and level <= 3, f"tried to set perf_event_paranoid to {level}, allowed values are -1 through 3"
25
+ cmd = f"sudo sysctl -w kernel.perf_event_paranoid={level}"
26
+ run_command_read_stdout(cmd)
27
+
28
+ def get_perf_event_paranoid() -> int:
29
+ """
30
+ Returns the current kernel perf_event_paranoid level
31
+ """
32
+ try:
33
+ with open("/proc/sys/kernel/perf_event_paranoid", "r") as f:
34
+ return int(f.read().strip())
35
+ except Exception as e:
36
+ raise RuntimeError(f"Failed to read perf_event_paranoid: {e}")
37
+
38
+
39
+ def interpret_umask(binval: str) -> str:
40
+ """
41
+ Convert a binary string umask into its hexadecimal representation.
42
+
43
+ Args:
44
+ binval (str): A string representing a binary number (e.g., "1101").
45
+
46
+ Returns:
47
+ str: The hexadecimal representation of the binary umask
48
+ (e.g., "0xd" for "1101").
49
+
50
+ Raises:
51
+ ValueError: If `binval` is not a valid binary string.
52
+ """
53
+ try:
54
+ val: int = int(binval, 2)
55
+ except:
56
+ raise ValueError(f"mask {binval} was not a valid binary string")
57
+
58
+ if val > 255:
59
+ raise ValueError(f"{binval} was more then 8 bits")
60
+
61
+ hex_str = str(hex(val))
62
+ return hex_str
63
+
64
+
65
+
66
+
67
+ def interpret_core_events(output: str, core_events: List[str]) -> Dict[str, int]:
68
+ """
69
+ Parse perf output for core events.
70
+
71
+ Args:
72
+ output (str): Raw stderr output from perf.
73
+ core_events (List[str]): List of event names to extract.
74
+
75
+ Returns:
76
+ Dict[str,int]: Mapping of event name -> event counter value.
77
+ """
78
+ lines = output.split("\n")
79
+ res: Dict[str, int] = {}
80
+ for line in lines:
81
+ for event in core_events:
82
+ if event in line:
83
+ # remove name of event
84
+ valstr = line.split(event)[0]
85
+ valstr = valstr.strip()
86
+
87
+ # remove commas
88
+ valstr = valstr.replace(",", "")
89
+
90
+ if "Byte" in valstr:
91
+
92
+ valstr = valstr.split("Byte")[0]
93
+
94
+ # TODO this is debatable whether you want this,
95
+ # Many events which are labeled byte
96
+ # cast from cache line to byte (ie: *64)
97
+ # so casting back (ie /64) is natural in most cases
98
+ val = int(int(valstr) / 64)
99
+ else:
100
+ val = int(valstr)
101
+ res[event] = val
102
+ return res
103
+ def interpret_per_core_event(output:str,event:str,socket:int) -> Dict[str,int]:
104
+ data : Dict[int,Dict[str,int]] = {skt:{} for skt in range(2)}
105
+ for line in output.split("\n"):
106
+ if event not in line:
107
+ continue
108
+ fields : List[str] = [x for x in line.split(";") if x != ""]
109
+ #print(fields)
110
+ #[S0,D0,C0]
111
+ core_code = fields[0].split("-")
112
+ socket_num = int(core_code[0][1:])
113
+ core = core_code[2][1:]
114
+ ctr = int(fields[2])
115
+ data[socket_num][core] = ctr
116
+ #print(data[socket])
117
+ return data[socket]
118
+ def perf_sample_per_core_event(cmd:str,event:str,socket:int) -> Dict[str,int]:
119
+ perf_cmd = f"sudo perf stat --per-core -x \\; -a -e {event} {cmd}"
120
+ output = run_command_read_stderr(perf_cmd)
121
+ return interpret_per_core_event(output,event,socket)
122
+ def perf_sample_per_core_events(cmd:str,events:List[str],socket:int) -> Dict[str,Dict[str,int]]:
123
+ eventstr = " ".join([f"-e {event}" for event in events])
124
+ perf_cmd = f"sudo perf stat --per-core -x \\; -a {eventstr} {cmd}"
125
+ output = run_command_read_stderr(perf_cmd)
126
+ return {event:interpret_per_core_event(output,event,socket) for event in events}
127
+ def perf_normalize_per_core_events(cmd:str,events:List[str],socket:int) -> Dict[str,Dict[str,float]]:
128
+ events += ["cycles"]
129
+ res = perf_sample_per_core_events(cmd,events,socket)
130
+ cycles = res["cycles"]
131
+ normalized_res = {event:{core:ctr/cycles[core] for core,ctr in percore.items()} for event,percore in res.items()}
132
+ return normalized_res
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+ def add_zeroes_to_eventcode(eventcode: str, zeroct: int):
141
+ raw_hex_str = eventcode.split("0x")[1]
142
+ return "0x" + ("0" * zeroct) + raw_hex_str
143
+
144
+ def perf_sample_core_events(cmd: str, core_events: List[str]) -> Dict[str, int]:
145
+ """
146
+ Run perf sampling for core events.
147
+
148
+ Args:
149
+ cmd (str): Command to run under perf.
150
+ core_events (List[str]): List of core event names.
151
+
152
+ Returns:
153
+ Dict[str,int]: Mapping of event name -> event counter value.
154
+ """
155
+
156
+ event_flags = [f"-e {e}" for e in core_events]
157
+ event_flag_str = " ".join(event_flags)
158
+
159
+ output = run_command_read_stderr(f"sudo perf stat {event_flag_str} {cmd}")
160
+ return interpret_core_events(output, core_events)
161
+ def perf_sample_core_event(cmd: str, core_event: str) -> int:
162
+ """
163
+ Run perf sampling for core event.
164
+
165
+ Args:
166
+ cmd (str): Command to run under perf.
167
+ core_event (str): core event name.
168
+
169
+ Returns:
170
+ int: core event value
171
+ """
172
+ return perf_sample_core_events(cmd,[core_event])[core_event]
@@ -0,0 +1,29 @@
1
+ from typing import Dict, List, Tuple
2
+ from rykit.perf_sample import perf_sample_core_events,interpret_umask
3
+ def perf_sample_amd_uncore_event_many(
4
+ cmd: str, unc_events: List[Tuple[str, str]]
5
+ ) -> Dict[str, int]:
6
+ """
7
+ Run perf sampling for multiple uncore events.
8
+
9
+ Args:
10
+ cmd (str): Command to run under perf.
11
+ unc_events (List[Tuple[str,str]]): List of (event, binary umask) pairs.
12
+
13
+ Returns:
14
+ Dict[str,Dict[str,int]]: Mapping of event code -> event counter value.
15
+ """
16
+ events = [f"amd_df/event={event},umask={interpret_umask(umask)}/" for event,umask in unc_events]
17
+ return perf_sample_core_events(cmd,events)
18
+ def perf_sample_core_event(cmd: str, core_event: str) -> int:
19
+ """
20
+ Run perf sampling for core event.
21
+
22
+ Args:
23
+ cmd (str): Command to run under perf.
24
+ core_event (str): core event name.
25
+
26
+ Returns:
27
+ int: core event value
28
+ """
29
+ return perf_sample_core_events(cmd,[core_event])[core_event]
@@ -0,0 +1,193 @@
1
+ import subprocess
2
+ import glob
3
+ from typing import Dict, List, Tuple
4
+ from rykit.perf_sample import interpret_umask, add_zeroes_to_eventcode
5
+ from rykit.intel_tools import get_cha_count
6
+ from rykit.cmd import run_command_read_stderr
7
+
8
+
9
+
10
+
11
+ def create_unc_cha_event(chanum: int, event: str, hexmask: str) -> str:
12
+ """
13
+ Create a perf event string for a single CHA.
14
+
15
+ Args:
16
+ chanum (int): CHA index.
17
+ event (str): Event code in hexadecimal (e.g., "0xb3").
18
+ hexmask (str): Umask in hexadecimal (e.g., "0x8").
19
+
20
+ Returns:
21
+ str: Perf event string for the CHA.
22
+ """
23
+ return f"uncore_cha_{chanum}/event={event},umask={hexmask}/"
24
+
25
+
26
+ def create_unc_cha_events(event: str, hexmask: str) -> List[str]:
27
+ """
28
+ Create perf event strings for all CHAs on the system.
29
+
30
+ Args:
31
+ event (str): Event code in hexadecimal.
32
+ hexmask (str): Umask in hexadecimal.
33
+
34
+ Returns:
35
+ List[str]: Perf event strings for each CHA.
36
+ """
37
+
38
+ num_chas = get_cha_count()
39
+ return [create_unc_cha_event(chanum, event, hexmask) for chanum in range(num_chas)]
40
+
41
+
42
+ def build_perf_sample_uncore_cmd(
43
+ program_cmd: str, unc_events: List[Tuple[str, str]]
44
+ ) -> str:
45
+ """
46
+ Build a perf command that samples multiple uncore events.
47
+
48
+ Args:
49
+ program_cmd (str): Command to run under perf.
50
+ unc_events (List[Tuple[str,str]]): List of (event, binary umask) pairs.
51
+
52
+ Returns:
53
+ str: The full perf command string.
54
+ """
55
+ events: List[str] = []
56
+ for event, mask in unc_events:
57
+ hexmask = interpret_umask(mask)
58
+ events += create_unc_cha_events(event, hexmask)
59
+ event_args: List[str] = [f"-e {e}" for e in events]
60
+ event_arg_str = " ".join(event_args)
61
+ return f"sudo perf stat -a {event_arg_str} -- {program_cmd}"
62
+
63
+
64
+ def interpret_uncore_event(output: str, event: str) -> Dict[str, int]:
65
+ """
66
+ Parse perf output for a single uncore event across CHAs.
67
+
68
+ Args:
69
+ output (str): Raw stderr output from perf.
70
+ event (str): Event code in hexadecimal.
71
+
72
+ Returns:
73
+ Dict[str,int]: Mapping of CHA index (as str) -> event counter value.
74
+ """
75
+ infix = "uncore_cha_"
76
+ number_suffix = f"/event={event}"
77
+ result: Dict[str, int] = {}
78
+ for line in output.split("\n"):
79
+ # check to ensure this is a cha line
80
+ if infix not in line or number_suffix not in line:
81
+ continue
82
+ # Example:
83
+ # ex: 8,795 uncore_cha_1/event=0xb3,umask=0x8/
84
+
85
+ # remove name of event
86
+ # ex ->: 8,795
87
+ count_str_with_commas = line.split(infix)[0]
88
+
89
+ # remove ,
90
+ # ex ->: 8795
91
+ count_str = count_str_with_commas.replace(",", "")
92
+
93
+ count = int(count_str)
94
+
95
+ # chanum comes right after infix
96
+ chanum = line.split(infix)[1].split(number_suffix)[0]
97
+ result[chanum] = count
98
+ return result
99
+
100
+
101
+ def interpret_uncore_event_many(
102
+ output: str, events: List[str]
103
+ ) -> Dict[str, Dict[str, int]]:
104
+ """
105
+ Parse perf output for multiple uncore events.
106
+
107
+ Args:
108
+ output (str): Raw stderr output from perf.
109
+ events (List[str]): List of event codes in hexadecimal.
110
+
111
+ Returns:
112
+ Dict[str,Dict[str,int]]: Mapping of event code ->
113
+ CHA index (as str) -> event counter value.
114
+ """
115
+ return {e: interpret_uncore_event(output, e) for e in events}
116
+
117
+
118
+ def perf_sample_uncore_event_many(
119
+ program_cmd: str, unc_events: List[Tuple[str, str]]
120
+ ) -> Dict[str, Dict[str, int]]:
121
+ """
122
+ Run perf sampling for multiple uncore events.
123
+
124
+ Args:
125
+ program_cmd (str): Command to run under perf.
126
+ unc_events (List[Tuple[str,str]]): List of (event, binary umask) pairs.
127
+
128
+ Returns:
129
+ Dict[str,Dict[str,int]]: Mapping of event code ->
130
+ CHA index (as str) -> event counter value.
131
+ """
132
+ assert isinstance(program_cmd, str), "cmd must be passed as string (passed non-str)"
133
+ assert len(unc_events) <= 4, "can only run upto 4 uncore events at once"
134
+
135
+ events = [e for e, _ in unc_events]
136
+ if len(events) != len(set(events)):
137
+ raise ValueError(
138
+ f'this function does not support passing two identical event codes (passed {events})\n\t hint: prepend 0 ie ["0xF", "0x0F","0x00F","0x000F"]'
139
+ )
140
+
141
+ for _, mask in unc_events:
142
+ assert isinstance(mask, str), "mask must be a binary string (passed non-str)"
143
+ assert set(mask) <= {"0", "1"}, f"mask must be a binary string (passed {mask})"
144
+
145
+ cmd = build_perf_sample_uncore_cmd(program_cmd, unc_events)
146
+
147
+ output = run_command_read_stderr(cmd)
148
+
149
+ events = [unce[0] for unce in unc_events]
150
+
151
+ return interpret_uncore_event_many(output, events)
152
+
153
+
154
+ def perf_sample_uncore_event(program_cmd: str, event: str, mask: str) -> Dict[str, int]:
155
+ """
156
+ Run perf sampling for a single uncore event.
157
+
158
+ Args:
159
+ program_cmd (str): Command to run under perf.
160
+ event (str): Event code in hexadecimal.
161
+ mask (str): Umask in binary string form.
162
+
163
+ Returns:
164
+ Dict[str,int]: Mapping of CHA index (as str) -> event counter value.
165
+ """
166
+ res = perf_sample_uncore_event_many(program_cmd, [(event, mask)])
167
+ return list(res.values())[0]
168
+
169
+
170
+
171
+
172
+ def perf_sample_uncore_event_many_named_masks(
173
+ cmd: str, eventcode: str, masks: Dict[str, str]
174
+ ) -> Dict[str, Dict[str, int]]:
175
+ # create a list of unique eventcodes for each mask by adding zeroes to RHS (ie 0xF, 0x0F, 0x00F, ...) so that we have U UID for event info from perf
176
+ name_to_code: dict[str, str] = {
177
+ k: add_zeroes_to_eventcode(eventcode, zeroct)
178
+ for zeroct, k in enumerate(masks.keys())
179
+ }
180
+ code_to_name: dict[str, str] = {v: k for k, v in name_to_code.items()}
181
+
182
+ uncore_events: List[Tuple[str, str]] = [
183
+ (code, masks[name]) for name, code in name_to_code.items()
184
+ ]
185
+
186
+ res_by_event_code: Dict[str, Dict[str, int]] = perf_sample_uncore_event_many(
187
+ cmd, uncore_events
188
+ )
189
+ # convert keys from eventcode to eventname
190
+ res_by_event_name: Dict[str, Dict[str, int]] = {
191
+ code_to_name[code]: ctr for code, ctr in res_by_event_code.items()
192
+ }
193
+ return res_by_event_name