#!/usr/bin/env python3 """config_sanity.py -- stdlib-only sanity check for the TU104 scaffold config. Parses gpgpu-sim/configs/turing-tu104-2080super/gpgpusim.config and asserts that the key device knobs and the machine-checkable `# SPEC:` markers are present and internally consistent: * SM count : n_clusters x n_cores_per_cluster == 48 * compute capability : major.minor == 7.5, PTX force max capability == 75 * memory type : GDDR6 (burst length 16, QDR freq ratio 4) * memory capacity : SPEC vram_gib == 8 (documentation-only; GPGPU-Sim 4.x has no VRAM capacity knob) * L2 size : sets x bsize x assoc x n_mem x n_sub == 4 MiB * bandwidth : buswidth x freq_ratio x DRAM clock x n_mem == 496 GB/s * clocks : base 1650 MHz, DRAM 3875 MHz (15.5 Gbps / 4) This is a scaffold consistency test, not a calibration: passing says the file internally encodes the published TU104 numbers, NOT that the simulator models TU104 accurately. Exit code 0 = all checks pass; 1 = at least one mismatch; 2 = usage/parse error. Usage: python3 gpgpu-sim/scripts/config_sanity.py [--config PATH] [--quiet] """ from __future__ import annotations import argparse import re import sys from pathlib import Path EXPECTED_SPECS = { "sm_count": "48", "memory_type": "GDDR6", "vram_gib": "8", "bus_width_bits": "256", "bandwidth_gbps": "496", "l2_mib": "4", "rops": "64", "tmus": "192", "base_mhz": "1650", "boost_mhz": "1815", "compute_capability": "7.5", } DEFAULT_CONFIG = ( Path(__file__).resolve().parents[1] / "configs" / "turing-tu104-2080super" / "gpgpusim.config" ) ICNT_NAME = "config_turing_islip.icnt" def parse_config(path: Path) -> tuple[dict[str, str], dict[str, str]]: """Return (options, spec_markers) parsed from a gpgpusim.config file.""" opts: dict[str, str] = {} specs: dict[str, str] = {} for raw in path.read_text(encoding="utf-8").splitlines(): spec = re.match(r"^\s*#\s*SPEC:\s*([A-Za-z0-9_]+)\s*=\s*(\S+)\s*$", raw) if spec: specs[spec.group(1)] = spec.group(2) line = raw.split("#", 1)[0].strip() if not line or not line.startswith("-"): continue parts = line.split(None, 1) opts[parts[0]] = parts[1].strip() if len(parts) > 1 else "" return opts, specs class Checker: def __init__(self, quiet: bool = False) -> None: self.quiet = quiet self.passed = 0 self.failures: list[str] = [] def check(self, name: str, condition: bool, detail: str) -> None: if condition: self.passed += 1 if not self.quiet: print(f"PASS: {name}: {detail}") else: self.failures.append(f"{name}: {detail}") print(f"FAIL: {name}: {detail}") def note(self, text: str) -> None: if not self.quiet: print(f"NOTE: {text}") def parse_int(opts: dict[str, str], key: str) -> int | None: try: return int(opts[key], 0) except (KeyError, ValueError): return None def parse_l2(opts: dict[str, str]) -> tuple[int, int, int] | None: """Parse '::' from -gpgpu_cache:dl2 S:... .""" m = re.match(r"^S:(\d+):(\d+):(\d+)", opts.get("-gpgpu_cache:dl2", "")) if not m: return None return int(m.group(1)), int(m.group(2)), int(m.group(3)) def run_checks(config_path: Path, quiet: bool = False) -> int: if not config_path.is_file(): print(f"config_sanity: ERROR: config not found: {config_path}", file=sys.stderr) return 2 opts, specs = parse_config(config_path) c = Checker(quiet=quiet) # 1. spec markers ------------------------------------------------------ for key, expected in EXPECTED_SPECS.items(): actual = specs.get(key) c.check( f"SPEC {key}", actual == expected, f"expected {expected}, got {actual!r}", ) # 2. SMs / compute capability ----------------------------------------- clusters = parse_int(opts, "-gpgpu_n_clusters") cores = parse_int(opts, "-gpgpu_n_cores_per_cluster") sm_count = None if clusters is None or cores is None else clusters * cores c.check( "SM count", sm_count == int(EXPECTED_SPECS["sm_count"]), f"n_clusters({clusters}) x n_cores_per_cluster({cores}) = {sm_count}," f" expected {EXPECTED_SPECS['sm_count']}", ) cc = f"{opts.get('-gpgpu_compute_capability_major', '?')}." \ f"{opts.get('-gpgpu_compute_capability_minor', '?')}" c.check( "compute capability", cc == EXPECTED_SPECS["compute_capability"], f"{cc}, expected {EXPECTED_SPECS['compute_capability']}", ) c.check( "PTX force max capability", parse_int(opts, "-gpgpu_ptx_force_max_capability") == 75, f"{opts.get('-gpgpu_ptx_force_max_capability', '(missing)')}, expected 75 (sm_75)", ) # 3. clocks / GDDR6 / memory geometry --------------------------------- clocks = opts.get("-gpgpu_clock_domains", "").split(":") dram_mhz = None if len(clocks) == 4: try: dram_mhz = float(clocks[3]) except ValueError: dram_mhz = None c.check( "DRAM clock", dram_mhz == 3875.0, f"{clocks[3] if len(clocks) == 4 else clocks}, expected 3875.0 MHz (15.5 Gbps / 4)", ) c.check( "base core clock", len(clocks) == 4 and clocks[0] == "1650.0", f"{clocks[0] if clocks else '(missing)'}, expected 1650.0 MHz", ) c.check( "memory type GDDR6", specs.get("memory_type") == "GDDR6" and parse_int(opts, "-gpgpu_dram_burst_length") == 16 and parse_int(opts, "-dram_data_command_freq_ratio") == 4, "burst_length=" f"{opts.get('-gpgpu_dram_burst_length', '(missing)')}, " "freq_ratio=" f"{opts.get('-dram_data_command_freq_ratio', '(missing)')}" " (GDDR6 expected: 16 / 4)", ) c.check( "memory chips per controller", parse_int(opts, "-gpgpu_n_mem_per_ctrlr") == 1, f"{opts.get('-gpgpu_n_mem_per_ctrlr', '(missing)')}, expected 1 (upstream Turing convention)", ) c.check( "VRAM capacity marker (documentation-only)", specs.get("vram_gib") == "8", "SPEC vram_gib=" f"{specs.get('vram_gib')!r}; GPGPU-Sim 4.x has no VRAM capacity knob," " so 8 GiB is recorded as a spec marker only", ) # 4. L2 size consistency ---------------------------------------------- n_mem = parse_int(opts, "-gpgpu_n_mem") n_sub = parse_int(opts, "-gpgpu_n_sub_partition_per_mchannel") l2 = parse_l2(opts) l2_mib = None if l2 and n_mem and n_sub: sets, bsize, assoc = l2 l2_mib = sets * bsize * assoc * n_mem * n_sub / (1024 * 1024) spec_l2 = float(EXPECTED_SPECS["l2_mib"]) c.check( "L2 size", l2_mib == spec_l2, f"dl2({l2}) x n_mem({n_mem}) x n_sub({n_sub}) = {l2_mib} MiB," f" expected {spec_l2} MiB", ) # 5. bandwidth consistency -------------------------------------------- buswidth = parse_int(opts, "-gpgpu_dram_buswidth") ratio = parse_int(opts, "-dram_data_command_freq_ratio") bw_gbps = None if None not in (buswidth, ratio, dram_mhz, n_mem): bw_gbps = buswidth * ratio * dram_mhz * 1e6 * n_mem / 1e9 spec_bw = float(EXPECTED_SPECS["bandwidth_gbps"]) c.check( "modeled bandwidth", bw_gbps == spec_bw, f"buswidth({buswidth} B) x ratio({ratio}) x DRAM({dram_mhz} MHz)" f" x n_mem({n_mem}) = {bw_gbps} GB/s, expected {spec_bw} GB/s", ) bw_bits_ok = False if n_mem and buswidth: bw_bits_ok = ( specs.get("bus_width_bits") == "256" and int(EXPECTED_SPECS["bus_width_bits"]) // (8 * n_mem) == buswidth ) c.check( "bus width marker", bw_bits_ok, f"SPEC bus_width_bits={specs.get('bus_width_bits')!r}, modeled" f" buswidth={buswidth} B x n_mem={n_mem} (expected {EXPECTED_SPECS['bus_width_bits']} bits" f" = {buswidth} B x {n_mem} modules)", ) # 6. informational markers not modeled by GPGPU-Sim 4.x --------------- for key, label in (("rops", "ROPs"), ("tmus", "TMUs"), ("boost_mhz", "boost clock")): c.check( f"SPEC {key} present", specs.get(key) == EXPECTED_SPECS[key], f"{label}: {specs.get(key)!r} (expected {EXPECTED_SPECS[key]});" " recorded for documentation, not a simulator knob", ) # 7. sibling interconnect file (not used with -network_mode 2) ------- icnt = config_path.parent / ICNT_NAME c.check( "interconnect config present", icnt.is_file(), f"{icnt}", ) total = c.passed + len(c.failures) print( f"config_sanity: {c.passed}/{total} checks passed" + (f", {len(c.failures)} failed" if c.failures else "") ) if c.failures: for failure in c.failures: print(f"config_sanity: FAILED -> {failure}", file=sys.stderr) return 1 print("config_sanity: OK (scaffold consistency only; no accuracy claim)") return 0 def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description="Sanity-check the GreenEye-GPGPU-Sim TU104 scaffold config." ) parser.add_argument( "--config", type=Path, default=DEFAULT_CONFIG, help=f"path to gpgpusim.config (default: {DEFAULT_CONFIG})", ) parser.add_argument("--quiet", action="store_true", help="only print failures and summary") args = parser.parse_args(argv) return run_checks(args.config, quiet=args.quiet) if __name__ == "__main__": sys.exit(main())