carm-roofline 1.0.0.dev4__tar.gz → 1.0.1.dev0__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.
Files changed (100) hide show
  1. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/PKG-INFO +5 -4
  2. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/README.md +2 -2
  3. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/architecture.py +41 -16
  4. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/config.py +8 -8
  5. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/detect.py +3 -3
  6. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/memory.py +7 -7
  7. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/benchmarking.py +17 -19
  8. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/x86.py +1 -1
  9. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/interface.py +6 -2
  10. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/memory.py +16 -2
  11. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/roofline.py +1 -1
  12. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm.py +45 -39
  13. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm_roofline.egg-info/PKG-INFO +5 -4
  14. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm_roofline.egg-info/SOURCES.txt +13 -0
  15. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm_roofline.egg-info/requires.txt +1 -0
  16. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm_roofline.egg-info/top_level.txt +2 -0
  17. carm_roofline-1.0.1.dev0/error.py +5 -0
  18. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/exec_interface.py +0 -2
  19. carm_roofline-1.0.1.dev0/profiling/__init__.py +151 -0
  20. carm_roofline-1.0.1.dev0/profiling/aggregation.py +215 -0
  21. carm_roofline-1.0.1.dev0/profiling/backends.py +179 -0
  22. carm_roofline-1.0.1.dev0/profiling/config.py +114 -0
  23. carm_roofline-1.0.1.dev0/profiling/loaders.py +172 -0
  24. carm_roofline-1.0.1.dev0/profiling/metrics.py +429 -0
  25. carm_roofline-1.0.1.dev0/profiling/model.py +143 -0
  26. carm_roofline-1.0.1.dev0/profiling/output.py +131 -0
  27. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/pyproject.toml +10 -7
  28. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/run_config.py +9 -1
  29. carm_roofline-1.0.1.dev0/test/test_isa_codegen_integration.py +958 -0
  30. carm_roofline-1.0.1.dev0/test/test_memory_suite_generation.py +178 -0
  31. carm_roofline-1.0.1.dev0/test/test_output_module.py +924 -0
  32. carm_roofline-1.0.1.dev0/test/test_typed_benchmarks.py +818 -0
  33. carm_roofline-1.0.1.dev0/utils.py +177 -0
  34. carm_roofline-1.0.1.dev0/workspace.py +44 -0
  35. carm_roofline-1.0.0.dev4/error.py +0 -5
  36. carm_roofline-1.0.0.dev4/utils.py +0 -314
  37. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/LICENSE +0 -0
  38. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/__init__.py +0 -0
  39. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/arm.py +0 -0
  40. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/frequency.py +0 -0
  41. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/riscv.py +0 -0
  42. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/arm/features.c +0 -0
  43. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/arm/frequency.h +0 -0
  44. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/arm/vlen.c +0 -0
  45. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/frequency.c +0 -0
  46. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/riscv/frequency.h +0 -0
  47. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/riscv/rvv_version.c +0 -0
  48. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/riscv/vlen.c +0 -0
  49. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/x86/cache.c +0 -0
  50. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/x86/features.c +0 -0
  51. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/x86/frequency.h +0 -0
  52. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/tests/x86/x86_avx512/frequency.h +0 -0
  53. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/architecture/x86.py +0 -0
  54. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/arguments.py +0 -0
  55. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/__init__.py +0 -0
  56. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/benchmark.py +0 -0
  57. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/__init__.py +0 -0
  58. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/arm.py +0 -0
  59. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/code_gen/__init__.py +0 -0
  60. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/code_gen/data_type.py +0 -0
  61. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/code_gen/instruction.py +0 -0
  62. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/code_gen/operation.py +0 -0
  63. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/code_gen/register.py +0 -0
  64. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/isa.py +0 -0
  65. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/parameters.py +0 -0
  66. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/generation/riscv.py +0 -0
  67. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/__init__.py +0 -0
  68. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/arithmetic.py +0 -0
  69. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/base.py +0 -0
  70. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/common.py +0 -0
  71. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/memory.py +0 -0
  72. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/memory_sweep.py +0 -0
  73. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/mixed.py +0 -0
  74. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/output/roofline.py +0 -0
  75. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/result.py +0 -0
  76. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/__init__.py +0 -0
  77. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/arithmetic.py +0 -0
  78. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/base.py +0 -0
  79. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/memory_sweep.py +0 -0
  80. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/benchmark/suites/mixed.py +0 -0
  81. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm_roofline.egg-info/dependency_links.txt +0 -0
  82. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/carm_roofline.egg-info/entry_points.txt +0 -0
  83. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/context.py +0 -0
  84. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/__init__.py +0 -0
  85. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/assets/CARM_icon.svg +0 -0
  86. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/assets/CHAMP_logo.svg +0 -0
  87. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/assets/menu_icon.png +0 -0
  88. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/assets/style.css +0 -0
  89. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/config.py +0 -0
  90. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/dashboard.py +0 -0
  91. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/gui/gui_utils.py +0 -0
  92. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/output_utils.py +0 -0
  93. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/results_paths.py +0 -0
  94. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/setup.cfg +0 -0
  95. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/test_bench/__init__.py +0 -0
  96. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/test_bench/builder.py +0 -0
  97. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/test_bench/test_bench.c +0 -0
  98. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/test_bench/test_bench.h +0 -0
  99. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/test_bench/wrapper.inl +0 -0
  100. {carm_roofline-1.0.0.dev4 → carm_roofline-1.0.1.dev0}/units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: carm-roofline
3
- Version: 1.0.0.dev4
3
+ Version: 1.0.1.dev0
4
4
  Summary: CARM: Cache-Aware Roofline Model benchmarking and visualization toolkit
5
5
  Author: CHaMP Team
6
6
  License: GNU LESSER GENERAL PUBLIC LICENSE
@@ -518,7 +518,7 @@ Classifier: Intended Audience :: Science/Research
518
518
  Classifier: Topic :: Software Development :: Testing
519
519
  Classifier: Topic :: System :: Benchmark
520
520
  Classifier: Programming Language :: Python :: 3
521
- Classifier: Programming Language :: Python :: 3.10
521
+ Classifier: Programming Language :: Python :: 3.9
522
522
  Classifier: Operating System :: POSIX :: Linux
523
523
  Requires-Python: >=3.9
524
524
  Description-Content-Type: text/markdown
@@ -529,6 +529,7 @@ Requires-Dist: numpy>=2.0.0
529
529
  Requires-Dist: matplotlib>=3.7.2
530
530
  Requires-Dist: tomli>=2.0.0
531
531
  Requires-Dist: platformdirs>=4.0.0
532
+ Requires-Dist: argcomplete>=3.0.0
532
533
  Provides-Extra: gui
533
534
  Requires-Dist: dash[diskcache]; extra == "gui"
534
535
  Requires-Dist: dash-bootstrap-components; extra == "gui"
@@ -622,7 +623,7 @@ Other usage examples:
622
623
  # Short roofline benchmark (1 second per micro-bench)
623
624
  carm benchmark --test roofline --test-time 1
624
625
  # Arithmetic-only
625
- carm benchmark --test arithmetic --instruction add --data-type f32
626
+ carm benchmark --test arithmetic --instruction add mul fma --data-type f32
626
627
  # Memory-only
627
628
  carm benchmark --test memory --mem-target L2 --ld-st-ratio 2:1
628
629
  # Dry run (generate benchmark code, skip compile/execute):
@@ -631,7 +632,7 @@ carm benchmark --dry-run --test arithmetic --test-time 1 --verbose 4
631
632
 
632
633
  ## Output and Results
633
634
 
634
- By default, results are written under the platform user data directory for the `carm` app (via platformdirs). On linux, this is typically `~/.local/share/carm/`, on Windows, `%APPDATA%\carm\`, and on MacOS, `~/Library/Application Support/carm/`.
635
+ By default, results are written to the user data directory (via platformdirs). On linux, this is typically `~/.local/share/carm/`, on Windows, `%APPDATA%\carm\`, and on MacOS, `~/Library/Application Support/carm/`.
635
636
 
636
637
  Control output directory and format(s):
637
638
 
@@ -75,7 +75,7 @@ Other usage examples:
75
75
  # Short roofline benchmark (1 second per micro-bench)
76
76
  carm benchmark --test roofline --test-time 1
77
77
  # Arithmetic-only
78
- carm benchmark --test arithmetic --instruction add --data-type f32
78
+ carm benchmark --test arithmetic --instruction add mul fma --data-type f32
79
79
  # Memory-only
80
80
  carm benchmark --test memory --mem-target L2 --ld-st-ratio 2:1
81
81
  # Dry run (generate benchmark code, skip compile/execute):
@@ -84,7 +84,7 @@ carm benchmark --dry-run --test arithmetic --test-time 1 --verbose 4
84
84
 
85
85
  ## Output and Results
86
86
 
87
- By default, results are written under the platform user data directory for the `carm` app (via platformdirs). On linux, this is typically `~/.local/share/carm/`, on Windows, `%APPDATA%\carm\`, and on MacOS, `~/Library/Application Support/carm/`.
87
+ By default, results are written to the user data directory (via platformdirs). On linux, this is typically `~/.local/share/carm/`, on Windows, `%APPDATA%\carm\`, and on MacOS, `~/Library/Application Support/carm/`.
88
88
 
89
89
  Control output directory and format(s):
90
90
 
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from arguments import InsertsArguments, positive_int
8
8
  from benchmark import generation as bench_gen
9
- from error import ConfigurationError
9
+ from error import UserError
10
10
  from output_utils import configure_verbosity, debug, detail, format_if_container, warn
11
11
  from units import Frequency
12
12
 
@@ -28,14 +28,14 @@ def check_isa_compatibility(selected_isas: list[type[BaseISA]]) -> None:
28
28
  families.add(family)
29
29
 
30
30
  if len(families) > 1:
31
- raise ConfigurationError(f"Incompatible ISA families selected: {', '.join(families)}")
31
+ raise UserError(f"Incompatible ISA families selected: {', '.join(families)}")
32
32
 
33
33
  # Check for special incompatibilities within the same family
34
34
  isa_set = set(selected_isas)
35
35
  for incompatible_pair in bench_gen.INCOMPATIBLE_ISAS:
36
36
  if incompatible_pair.issubset(isa_set):
37
37
  pair_names = ", ".join(isa.name for isa in incompatible_pair)
38
- raise ConfigurationError(f"Incompatible ISAs selected: {pair_names}")
38
+ raise UserError(f"Incompatible ISAs selected: {pair_names}")
39
39
 
40
40
 
41
41
  def positive_po2_int(arg: str) -> int:
@@ -103,6 +103,36 @@ class ISAFrequencies:
103
103
  base_frequency = Frequency(base_frequency)
104
104
  return ISAFrequencies({isa.name: base_frequency for isa in isas})
105
105
 
106
+ @staticmethod
107
+ def from_detected(detected: DetectedArchitecture, isa_list: list[type[BaseISA]]) -> ISAFrequencies:
108
+ """Create ISAFrequencies from auto-detected architecture data.
109
+
110
+ Initializes all ISAs to the base frequency, then overrides with any per-ISA
111
+ specific frequencies that were detected.
112
+
113
+ Args:
114
+ detected: DetectedArchitecture object from probing
115
+ isa_list: List of ISA classes for this architecture
116
+
117
+ Returns:
118
+ ISAFrequencies with appropriate frequencies from detected data
119
+
120
+ Raises:
121
+ ValueError: If no frequency information is available
122
+ """
123
+ base_freq = detected.frequency
124
+ if not base_freq:
125
+ raise ValueError("Frequency could not be auto-detected, specify it using --frequency <freq>")
126
+
127
+ # Start with all ISAs at base frequency
128
+ isa_freqs_dict = {isa.name: base_freq for isa in isa_list}
129
+
130
+ # Override with any specific ISA frequencies that were detected
131
+ if detected.isa_frequencies:
132
+ isa_freqs_dict.update(detected.isa_frequencies)
133
+
134
+ return ISAFrequencies(isa_freqs_dict)
135
+
106
136
  def __getitem__(self, isa_name: str) -> Frequency:
107
137
  """Get frequency for a specific ISA.
108
138
 
@@ -111,6 +141,9 @@ class ISAFrequencies:
111
141
 
112
142
  Returns:
113
143
  Frequency wrapper for that ISA
144
+
145
+ Raises:
146
+ KeyError: If ISA not found
114
147
  """
115
148
  return self._isa_frequencies[isa_name]
116
149
 
@@ -177,7 +210,7 @@ class Architecture(InsertsArguments):
177
210
  "Specify them using a TOML config file with --topology-config <path>"
178
211
  )
179
212
 
180
- # Handle frequency: user arg (Frequency wrapper) overrides all, otherwise use detected per-ISA frequencies
213
+ # Handle frequency: user arg (Frequency wrapper) overrides all, otherwise use detected
181
214
  if args.frequency is not None:
182
215
  # args.frequency is already a Frequency wrapper from frequency_type()
183
216
  self.frequency = ISAFrequencies.from_base_frequency(args.frequency, self.isa)
@@ -185,14 +218,7 @@ class Architecture(InsertsArguments):
185
218
  d_freq = detected.frequency if detected.isa_frequencies is None else detected.isa_frequencies
186
219
  _override_warn("frequency", args.frequency, d_freq)
187
220
  else:
188
- isa_freqs = detected.isa_frequencies
189
- # if there are isa-specific frequencies, use them; otherwise fall back to base frequency if available
190
- if isa_freqs:
191
- self.frequency = ISAFrequencies(isa_freqs)
192
- elif base_freq := detected.frequency:
193
- self.frequency = ISAFrequencies.from_base_frequency(base_freq, self.isa)
194
- else:
195
- raise ValueError("Frequency could not be auto-detected, specify it using --frequency <freq>")
221
+ self.frequency = ISAFrequencies.from_detected(detected, self.isa)
196
222
 
197
223
  self.nominal_frequency: Frequency | None = detected.frequency_nominal
198
224
 
@@ -252,20 +278,18 @@ class Architecture(InsertsArguments):
252
278
  parser.add_argument(
253
279
  "-i",
254
280
  "--isa",
255
- nargs="*",
281
+ nargs="+",
256
282
  choices=list(ISA_NAME_TO_CLASS.keys()),
257
283
  help="Set of ISAs to test. (Default: automatically detects all available ISAs)",
258
284
  )
259
285
  parser.add_argument(
260
286
  "-l",
261
287
  "--vector-length",
262
- nargs="?",
263
288
  type=positive_po2_int,
264
289
  help="Vector register length in bytes. (Default: automatically detected)",
265
290
  )
266
291
  parser.add_argument(
267
292
  "--vector-lmul",
268
- nargs="?",
269
293
  type=int,
270
294
  choices=(1, 2, 4, 8),
271
295
  help="Vector register group modifier (LMUL). RISC-V specific.",
@@ -283,12 +307,13 @@ class Architecture(InsertsArguments):
283
307
  "--emit-config",
284
308
  type=str,
285
309
  metavar="PATH",
310
+ nargs="?",
311
+ const="topology.toml",
286
312
  help="Generate a template TOML configuration file at the specified path and exit. "
287
313
  "Edit the template and pass it back with --topology-config.",
288
314
  )
289
315
  parser.add_argument(
290
316
  "--frequency",
291
- nargs="?",
292
317
  type=Frequency.from_argparse,
293
318
  help="Processor frequency, e.g. '2GHz', '3200mhz' (Default: automatically detected if possible)",
294
319
  )
@@ -18,7 +18,7 @@ from __future__ import annotations
18
18
  from pathlib import Path
19
19
  from typing import cast
20
20
 
21
- from error import ConfigurationError
21
+ from error import UserError
22
22
  from units import Bytes
23
23
 
24
24
  from .memory import SimpleMemoryTopology
@@ -81,13 +81,13 @@ def load_memory_topology_from_toml(config_path: Path | str) -> SimpleMemoryTopol
81
81
  """
82
82
  config_path = Path(config_path)
83
83
  if not config_path.exists():
84
- raise ConfigurationError(f"Config file not found: {config_path}")
84
+ raise UserError(f"Config file not found: {config_path}")
85
85
 
86
86
  try:
87
87
  with open(config_path, "rb") as f:
88
88
  config = tomllib.load(f)
89
89
  except Exception as e:
90
- raise ConfigurationError(f"Failed to parse TOML file '{config_path}': {e}") from e
90
+ raise UserError(f"Failed to parse TOML file '{config_path}': {e}") from e
91
91
 
92
92
  total_cpus = _read_optional_int(config, "total_cpus")
93
93
  smt_degree = cast(int, _read_optional_int(config, "smt_degree", 1))
@@ -96,7 +96,7 @@ def load_memory_topology_from_toml(config_path: Path | str) -> SimpleMemoryTopol
96
96
  # Extract cache levels
97
97
  cache_levels_data = config.get("cache_levels", [])
98
98
  if not isinstance(cache_levels_data, list) or not cache_levels_data:
99
- raise ConfigurationError("Configuration must contain at least one [[cache_levels]] section")
99
+ raise UserError("Configuration must contain at least one [[cache_levels]] section")
100
100
 
101
101
  # Parse and validate each cache level (processed in document order; order determines level numbers)
102
102
  level_sizes: list[Bytes] = []
@@ -104,17 +104,17 @@ def load_memory_topology_from_toml(config_path: Path | str) -> SimpleMemoryTopol
104
104
 
105
105
  for level_dict in cache_levels_data:
106
106
  if not isinstance(level_dict, dict):
107
- raise ConfigurationError(f"Each [[cache_levels]] entry must be a table, got: {type(level_dict).__name__}")
107
+ raise UserError(f"Each [[cache_levels]] entry must be a table, got: {type(level_dict).__name__}")
108
108
  try:
109
109
  instances = level_dict["instances"]
110
110
  size_str = level_dict["size"]
111
111
  except KeyError as e:
112
- raise ConfigurationError(f"Cache level section missing required field: {e}") from e
112
+ raise UserError(f"Cache level section missing required field: {e}") from e
113
113
 
114
114
  if not isinstance(instances, int):
115
- raise ConfigurationError(f"'instances' must be an integer, got: {instances!r}")
115
+ raise UserError(f"'instances' must be an integer, got: {instances!r}")
116
116
  if not isinstance(size_str, str):
117
- raise ConfigurationError(f"'size' must be a string, got: {size_str!r}")
117
+ raise UserError(f"'size' must be a string, got: {size_str!r}")
118
118
 
119
119
  size = Bytes.from_string(size_str)
120
120
  level_sizes.append(size)
@@ -15,7 +15,7 @@ from dataclasses import dataclass
15
15
  from pathlib import Path
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
- from error import ConfigurationError
18
+ from error import UserError
19
19
  from output_utils import debug, detail, warn
20
20
  from units import Frequency
21
21
 
@@ -244,7 +244,7 @@ def detect_cache(ctx: TestContext) -> dict[str, Any]:
244
244
  else:
245
245
  raise ValueError("MemoryTopology parsed but contains no CPU data")
246
246
  except Exception as e:
247
- raise ConfigurationError(
247
+ raise UserError(
248
248
  f"Failed to parse native system memory topology from /sys: {e}. "
249
249
  "For non-native execution or when sysfs is unavailable, "
250
250
  "please provide cache hierarchy explicitly with --topology-config."
@@ -424,7 +424,7 @@ def native_detect(threads: int = 1) -> DetectedArchitecture:
424
424
  """
425
425
  machine = platform.machine().lower()
426
426
  if machine == "":
427
- raise ConfigurationError(
427
+ raise UserError(
428
428
  "Unable to detect machine architecture (platform.machine() returned empty string). Please specify the "
429
429
  "architecture manually (--isa <arch>)."
430
430
  )
@@ -9,7 +9,7 @@ from enum import Enum
9
9
  from pathlib import Path
10
10
  from typing import Protocol, runtime_checkable
11
11
 
12
- from error import ConfigurationError
12
+ from error import UserError
13
13
  from output_utils import debug, format_if_container
14
14
  from units import Bytes
15
15
 
@@ -168,26 +168,26 @@ class SimpleMemoryTopology:
168
168
  """
169
169
 
170
170
  if len(level_sizes) != len(instances_per_level):
171
- raise ConfigurationError(
171
+ raise UserError(
172
172
  "Level sizes and instances_per_level must have same length: "
173
173
  f"{len(level_sizes)} vs {len(instances_per_level)}"
174
174
  )
175
175
  if not level_sizes:
176
- raise ConfigurationError("At least one cache level must be provided")
176
+ raise UserError("At least one cache level must be provided")
177
177
 
178
178
  if any(inst <= 0 for inst in instances_per_level):
179
- raise ConfigurationError("All values in instances_per_level must be positive integers")
179
+ raise UserError("All values in instances_per_level must be positive integers")
180
180
 
181
181
  if total_cpus is not None and total_cpus <= 0:
182
- raise ConfigurationError(f"total_cpus must be positive when provided, got {total_cpus}")
182
+ raise UserError(f"total_cpus must be positive when provided, got {total_cpus}")
183
183
 
184
184
  if cpu_offset < 0:
185
- raise ConfigurationError(f"cpu_offset must be >= 0, got {cpu_offset}")
185
+ raise UserError(f"cpu_offset must be >= 0, got {cpu_offset}")
186
186
 
187
187
  if total_cpus is not None:
188
188
  for idx, instances in enumerate(instances_per_level, 1):
189
189
  if instances > total_cpus:
190
- raise ConfigurationError(
190
+ raise UserError(
191
191
  f"Cache level {idx}: instances ({instances}) cannot exceed total_cpus ({total_cpus})"
192
192
  )
193
193
 
@@ -11,7 +11,7 @@ from enum import Enum
11
11
  from arguments import InsertsArguments, enum_action, positive_float, positive_int
12
12
  from benchmark.generation.code_gen.operation import ArithmeticOperation
13
13
  from output_utils import warn
14
- from units import Operations
14
+ from units import Bytes, Operations
15
15
 
16
16
  from .generation import DataType
17
17
 
@@ -66,27 +66,29 @@ def ld_st_ratio_type(arg: str) -> LoadStoreRatio:
66
66
  return LoadStoreRatio(ld, st)
67
67
 
68
68
 
69
- def mem_test_size_type(arg: str) -> int | str:
69
+ def mem_test_size_type(arg: str) -> Bytes | None:
70
70
  """Parse a memory test size specifier.
71
71
 
72
72
  Accepts either:
73
- - A positive integer (KiB)
73
+ - A quantity with a binary unit (e.g. "16 KiB", "2 MiB", "8 GiB")
74
74
  - The string "auto" (case-insensitive) for automatic sizing
75
75
 
76
76
  Returns:
77
- int (KiB value) or str (literal "auto")
77
+ Bytes or None (for "auto")
78
78
 
79
79
  Raises:
80
80
  argparse.ArgumentTypeError on invalid input.
81
81
  """
82
82
  s = str(arg).strip().lower()
83
83
  if s == "auto":
84
- return "auto"
84
+ return None
85
85
  try:
86
- return positive_int(arg)
87
- except (ValueError, argparse.ArgumentTypeError) as e:
86
+ return Bytes.from_argparse(arg)
87
+ except argparse.ArgumentTypeError:
88
+ raise
89
+ except Exception as e:
88
90
  raise argparse.ArgumentTypeError(
89
- f"invalid memory test size: {arg!r}. Must be a positive integer or 'auto'"
91
+ f"invalid memory test size: {arg!r}. Expected a binary unit string like '16 KiB', '2 MiB', or 'auto'. ({e})"
90
92
  ) from e
91
93
 
92
94
 
@@ -106,7 +108,7 @@ class Benchmarking(InsertsArguments):
106
108
  self.num_ops: Operations = Operations(args.num_ops)
107
109
  self.ld_st_ratio: LoadStoreRatio = args.ld_st_ratio
108
110
  self.arith_mem_ratio: int = args.arith_mem_ratio
109
- self.mem_test_sizes: list[int | str] | None = args.mem_test_sizes
111
+ self.mem_test_sizes: list[Bytes | None] | None = args.mem_test_sizes
110
112
  self.verbose: int = args.verbose
111
113
  self.test_time: float = args.test_time
112
114
  if self.test_time < 10.0:
@@ -132,7 +134,6 @@ class Benchmarking(InsertsArguments):
132
134
  "-m",
133
135
  "--mem-target",
134
136
  default="all",
135
- nargs="?",
136
137
  choices=["L1", "L2", "L3", "DRAM", "all"],
137
138
  help="Target memory level for 'memory' and 'mixed' tests (Default: all)",
138
139
  )
@@ -140,7 +141,6 @@ class Benchmarking(InsertsArguments):
140
141
  "-o",
141
142
  "--num-ops",
142
143
  default=32 * 1024,
143
- nargs="?",
144
144
  type=positive_int,
145
145
  help="Number of arithmetic operations to perform in the arithmetic test (Default: 32768)",
146
146
  )
@@ -148,7 +148,6 @@ class Benchmarking(InsertsArguments):
148
148
  "-d",
149
149
  "--data-type",
150
150
  default=DataType.f32,
151
- nargs="?",
152
151
  action=enum_action(DataType),
153
152
  help="Data type for benchmark operations (Default: f32)",
154
153
  )
@@ -171,7 +170,6 @@ class Benchmarking(InsertsArguments):
171
170
  parser.add_argument(
172
171
  "--ld-st-ratio",
173
172
  default=LoadStoreRatio(2, 1),
174
- nargs="?",
175
173
  type=ld_st_ratio_type,
176
174
  help="Load-to-store ratio for memory access patterns. Format: 'LD:ST' (e.g., '2:1') or a single "
177
175
  "integer for 'N:1' ratio. Use '1:0' for load-only or '0:1' for store-only tests. (Default: 2:1)",
@@ -179,22 +177,22 @@ class Benchmarking(InsertsArguments):
179
177
  parser.add_argument(
180
178
  "--arith-mem-ratio",
181
179
  default=2,
182
- nargs="?",
183
180
  type=positive_int,
184
181
  help="Ratio between arithmetic and memory operations for 'mixed' test (Default: 2)",
185
182
  )
186
183
  parser.add_argument(
187
184
  "--mem-test-sizes",
188
- nargs="*",
185
+ nargs="+",
189
186
  type=mem_test_size_type,
190
- help="Size of the test arrays for each memory level in KiB. 'auto' can be used to automatically determine "
191
- "sizes based on the cache sizes, e.g. '--mem-test-sizes 32 256 2048 65536' / '--mem-test-sizes auto auto "
192
- "2048 auto' for L1, L2, L3, DRAM tests respectively. (Default: auto for all levels)",
187
+ help="Size of the test arrays for each memory level. Accepts binary unit strings like '16KiB', '2MiB', "
188
+ "'8GiB', or 'auto' to automatically size based on detected caches. "
189
+ "e.g. '--mem-test-sizes 16KiB 256KiB 2MiB 64GiB' / "
190
+ "'--mem-test-sizes auto auto 2MiB auto' for L1, L2, L3, DRAM respectively. "
191
+ "(Default: auto for all levels)",
193
192
  )
194
193
  parser.add_argument(
195
194
  "--test-time",
196
195
  default=25.0,
197
- nargs="?",
198
196
  type=positive_float,
199
197
  help="Target runtime for each individual microbenchmark, in seconds. Low runtime may lead to inaccurate "
200
198
  "results. (Default: 25.0)",
@@ -172,7 +172,7 @@ class X86AVX2(X86AVX):
172
172
  super().__init__(**kwargs)
173
173
 
174
174
 
175
- class X86AVX512(X86Scalar):
175
+ class X86AVX512(X86SSE):
176
176
  name = "x86_avx512"
177
177
  BITS = 512
178
178
 
@@ -9,7 +9,6 @@ This module coordinates the entire benchmark pipeline:
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- import tempfile
13
12
  from pathlib import Path
14
13
  from typing import TYPE_CHECKING
15
14
 
@@ -20,6 +19,7 @@ from test_bench.builder import (
20
19
  create_microbenchmark_header,
21
20
  run_microbenchmarks,
22
21
  )
22
+ from workspace import workspace_context
23
23
 
24
24
  from .benchmark import BaseBenchmark
25
25
  from .result import parse_benchmark_output
@@ -120,9 +120,13 @@ def run_full_benchmark(
120
120
 
121
121
  debug(f"Flattened benchmarks for compilation: {list(flat_benchmarks.keys())}")
122
122
 
123
- with tempfile.TemporaryDirectory(prefix="carm-test-bench-") as workspace_dir:
123
+ keep_workspace = context.run_config.dry_run or context.run_config.keep_artifacts
124
+ with workspace_context(keep=keep_workspace, prefix="carm-benchmark-") as workspace_dir:
124
125
  workspace = Path(workspace_dir)
125
126
 
127
+ if keep_workspace:
128
+ detail(f"Artifacts will be kept in: {workspace}")
129
+
126
130
  # Keep generated artifacts in a writable temporary workspace.
127
131
  # Static C sources remain in-package and are included explicitly at compile time.
128
132
  generated_header = workspace / "microbenchmarks.h"
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections import defaultdict
6
+ from collections.abc import Iterator
6
7
  from dataclasses import dataclass
7
8
  from typing import TYPE_CHECKING
8
9
 
@@ -68,6 +69,11 @@ class MemoryBenchmarkSuite(ISABenchmarkSuite):
68
69
 
69
70
  previous_size_per_thread: Bytes | None = None
70
71
 
72
+ # Iterator over user-provided per-level sizes (if any)
73
+ user_sizes: Iterator[Bytes | None] | None = None
74
+ if benchmark.mem_test_sizes is not None:
75
+ user_sizes = iter(benchmark.mem_test_sizes)
76
+
71
77
  mem_level_indices = architecture.memory_topology.available_cache_levels()
72
78
  # Generate memory benchmarks for each memory level
73
79
  for mem_level_idx, mem_level_info in zip(mem_level_indices, architecture.memory_topology):
@@ -76,7 +82,7 @@ class MemoryBenchmarkSuite(ISABenchmarkSuite):
76
82
  # Plan thread affinity for this memory level
77
83
  thread_affinity = architecture.memory_topology.plan_thread_affinity(benchmark.threads, mem_level_idx)
78
84
  avail_size_per_thread: Bytes = thread_affinity.total_cache_bytes // thread_affinity.num_threads
79
- is_first_target = mem_level_idx == mem_level_indices[0]
85
+ _is_first_target = mem_level_idx == mem_level_indices[0]
80
86
  is_final_target = mem_level_idx == mem_level_indices[-1]
81
87
 
82
88
  if is_final_target and previous_size_per_thread is not None:
@@ -84,6 +90,13 @@ class MemoryBenchmarkSuite(ISABenchmarkSuite):
84
90
  else:
85
91
  target_size_per_thread = avail_size_per_thread * _CACHE_COVERAGE
86
92
 
93
+ # Override with user-provided size (if any) for this cache level.
94
+ # A None entry in the list (from "auto") keeps the computed value.
95
+ if user_sizes is not None:
96
+ user_size = next(user_sizes, None)
97
+ if user_size is not None:
98
+ target_size_per_thread = user_size
99
+
87
100
  # Warn if the per-thread dataset also fits in a smaller (closer-to-CPU) cache level.
88
101
  # E.g. targeting L3 with 16 threads, but per-thread data fits in each thread's L2:
89
102
  # the hardware will serve requests from L2, not L3, skewing the bandwidth result.
@@ -99,7 +112,8 @@ class MemoryBenchmarkSuite(ISABenchmarkSuite):
99
112
  )
100
113
 
101
114
  # Generate memory benchmark for this level
102
- layout_mode = MemoryLayoutMode.single if is_first_target else MemoryLayoutMode.split
115
+ # Use split layout only for the DRAM
116
+ layout_mode = MemoryLayoutMode.split if is_final_target else MemoryLayoutMode.single
103
117
  params = MemoryBenchmarkParams(
104
118
  data_type=data_type,
105
119
  thread_affinity=thread_affinity.cpu_ids,
@@ -65,7 +65,7 @@ class RooflineBenchmarkSuite(ISABenchmarkSuite):
65
65
  for bench in self.get_memory_benchmarks().values():
66
66
  if bench.cache_level is None:
67
67
  raise ValueError(
68
- f"Memory benchmark {bench.name} missing cache level annotation for roofline benchmarking"
68
+ f"Memory benchmark {bench.name} missing cache level annotation for roofline benchmarki>ng"
69
69
  )
70
70
  if bench.results is None:
71
71
  raise ValueError(f"Memory benchmark {bench.name} missing bandwidth result for roofline benchmarking")