From ff0730e949c15bce76006283aecae4fc5fba16e5 Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Sat, 27 Dec 2025 14:09:56 -0800 Subject: [PATCH 01/11] tests.sh executes --- examples/python_fraction/python_fraction.tex | 72 ++++++++++++++++++++ examples/python_fraction/tests.sh | 72 ++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 examples/python_fraction/python_fraction.tex create mode 100755 examples/python_fraction/tests.sh diff --git a/examples/python_fraction/python_fraction.tex b/examples/python_fraction/python_fraction.tex new file mode 100644 index 000000000..5eed1082f --- /dev/null +++ b/examples/python_fraction/python_fraction.tex @@ -0,0 +1,72 @@ +\documentclass{article} + +% Language setting +% Replace `english' with e.g. `spanish' to change the document language +\usepackage[english]{babel} + +% Set page size and margins +% Replace `letterpaper' with`a4paper' for UK/EU standard size +\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} + +% Useful packages +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{amsthm} +\usepackage{amsfonts} +\usepackage{graphicx} +\usepackage{algorithm2e} +\usepackage{enumitem} +\usepackage{comment} +\usepackage{natbib} + +\usepackage[colorlinks=true, allcolors=blue]{hyperref} + +\newcommand{\sdag}[1]{{#1}^{\dag}} + +\title{Fraction of Time MPI-SPPY spends in Python} +\author{ David L Woodruff\\ + Graduate School of Management\\ + \\ + University of California Davis\\ + Davis CA 95616 USA} +\date{\today} + +\newtheorem{theorem}{Theorem} +\newtheorem{lemma}{Lemma} + +\begin{document} +\maketitle + +When considering applications in practice, or when comparing to other +packages a question arises concerning the fraction of time that +mpi-sppy spends ``in Python'' as opposed to compiled code written in +other languages such as C and Fortran. Since solvers, numpy, and MPI +are all in the latter category, {\em a priori} one expects that the +fraction spent in Python will be small for all but toy problems. + + +\end{document} + +scalene is designed to attribute time to Python vs native (it can estimate time spent in compiled code called from Python). It’s often the most straightforward way to get exactly what you asked. + +Run: + +python -m pip install scalene +python -m scalene your_script.py [args...] + + +It reports per-file and per-line: + +Python time + +Native time (extensions, libraries like NumPy, solver bindings, etc.) + +MPI note: if you run under mpiexec, you’ll get output per rank (may need to direct to separate files): + +mpiexec -np 4 python -m scalene --outfile scalene_rank_%r.txt your_script.py ... + + +If %r isn’t supported in your shell, just set unique outfile names using env vars per rank. + +This is usually the quickest route to “fraction spent in Python.” + diff --git a/examples/python_fraction/tests.sh b/examples/python_fraction/tests.sh new file mode 100755 index 000000000..7606e526a --- /dev/null +++ b/examples/python_fraction/tests.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# wrap the mpiexec run in python so the rank can be determined +# run with $ SOLVER=gurobi mpiexec -np 3 ./tests.sh +# (do chmod once) + +set -euo pipefail + +SOLVER="${SOLVER:-gurobi}" + +# Determine MPI rank from common env vars (OpenMPI / MPICH / Slurm) +RANK="${OMPI_COMM_WORLD_RANK:-${PMI_RANK:-${SLURM_PROCID:-}}}" +if [[ -z "${RANK}" ]]; then + echo "Could not determine MPI rank from environment (OMPI_COMM_WORLD_RANK / PMI_RANK / SLURM_PROCID)." >&2 + exit 1 +fi + +echo "^^^ farmer ^^^ rank=${RANK}" + +python -m scalene run \ + --outfile "scalene_rank_${RANK}.txt" \ + ../../mpisppy/generic_cylinders.py \ + --module-name ../farmer/farmer \ + --num-scens 3 \ + --solver-name "${SOLVER}" \ + --max-iterations 10 \ + --max-solver-threads 4 \ + --default-rho 1 \ + --lagrangian \ + --xhatshuffle \ + --rel-gap 0.01 + +exit + +============================================== +#!/bin/bash +set -e + +SOLVER="gurobi" + +echo "^^^ farmer ^^^" +mpiexec -np 3 python -m scalene run --outfile scalene_rank_%r.txt ../../mpisppy/generic_cylinders.py --module-name ../farmer/farmer --num-scens 3 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 + +exit + +echo "^^^ sslp bounds ^^^" +cd sslp +mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name sslp --sslp-data-path ./data --instance-name sslp_15_45_10 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 +cd .. + +scalene is designed to attribute time to Python vs native (it can estimate time spent in compiled code called from Python). It’s often the most straightforward way to get exactly what you asked. + +Run: + +python -m pip install scalene +python -m scalene your_script.py [args...] + + +It reports per-file and per-line: + +Python time + +Native time (extensions, libraries like NumPy, solver bindings, etc.) + +MPI note: if you run under mpiexec, you’ll get output per rank (may need to direct to separate files): + +mpiexec -np 4 python -m scalene --outfile scalene_rank_%r.txt your_script.py ... + + +If %r isn’t supported in your shell, just set unique outfile names using env vars per rank. + +This is usually the quickest route to “fraction spent in Python.” From 895265ccbb9e4e04c99cc1cdd35602b3abd168a6 Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Sat, 27 Dec 2025 16:14:18 -0800 Subject: [PATCH 02/11] created a report concerning fraction of time spent in python --- examples/python_fraction/farmer_summary.bash | 6 + .../make_scalene_latex_table.py | 619 ++++++++++++++++++ examples/python_fraction/python_fraction.tex | 267 +++++++- examples/python_fraction/readme.rst | 10 + examples/python_fraction/test_sslp.sh | 24 + examples/python_fraction/tests.sh | 4 +- examples/sslp/sslp_demo.bash | 3 +- 7 files changed, 929 insertions(+), 4 deletions(-) create mode 100644 examples/python_fraction/farmer_summary.bash create mode 100644 examples/python_fraction/make_scalene_latex_table.py create mode 100644 examples/python_fraction/readme.rst create mode 100755 examples/python_fraction/test_sslp.sh diff --git a/examples/python_fraction/farmer_summary.bash b/examples/python_fraction/farmer_summary.bash new file mode 100644 index 000000000..947c27087 --- /dev/null +++ b/examples/python_fraction/farmer_summary.bash @@ -0,0 +1,6 @@ +python make_scalene_latex_table.py \ + --glob "scalene_rank_*.json" \ + --out scalene_summary_farmer.tex \ + --reduced \ + --cache-cli \ + --columns 200 diff --git a/examples/python_fraction/make_scalene_latex_table.py b/examples/python_fraction/make_scalene_latex_table.py new file mode 100644 index 000000000..90e476b33 --- /dev/null +++ b/examples/python_fraction/make_scalene_latex_table.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 +""" +make_scalene_latex_table.py + +Generate a LaTeX summary table from Scalene MPI rank profiles. + +Inputs: + scalene_rank_0.json, scalene_rank_1.json, ... + +Outputs: + A LaTeX file containing: + - System information (CPU + memory) collected from standard Unix tools/files + - Run parameters extracted from JSON argv (best-effort) + - A per-rank table with: + Wall (s): elapsed_time_sec from JSON + Python (s), Native (s), System (s): derived by parsing `scalene view --cli --reduced` + and summing per-line percentage columns (Time Python / native / system), then + multiplying by wall time. + +Totals row: + - Job wall time = max wall time across ranks + - Sum(Python seconds), Sum(Native seconds), Sum(System seconds) across ranks (if available) + +Why this approach: + With Scalene 2.0.1, `scalene run` emits JSON profiles whose CPU summary fields may be null. + The `scalene view --cli` output contains per-line "% of time" columns; summing those + percentages yields overall percent-of-wall-time totals. + +Usage: + python make_scalene_latex_table.py --glob "scalene_rank_*.json" --out scalene_summary.tex + +Options: + --cache-cli Cache CLI output as .cli.txt (reused if present) + --no-view Don't run scalene view (wall time only) + --reduced Pass --reduced to scalene view (recommended) + --columns N Set terminal width (COLUMNS) for scalene view output (default 200) +""" + +from __future__ import annotations + +import argparse +import glob +import json +import os +import platform +import re +import subprocess +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + + +@dataclass +class RankRow: + filename: str + rank: Optional[int] + wall_time: Optional[float] + python_time: Optional[float] + native_time: Optional[float] + system_time: Optional[float] + + +# --------------------------- +# JSON + misc helpers +# --------------------------- + +def _load_json(path: str) -> Dict[str, Any]: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def _coerce_float(x: Any) -> Optional[float]: + try: + if x is None: + return None + return float(x) + except Exception: + return None + + +def _parse_rank_from_filename(fname: str) -> Optional[int]: + m = re.search(r"rank[_\-]?(\d+)", os.path.basename(fname)) + if m: + return int(m.group(1)) + return None + + +def _latex_escape(s: str) -> str: + replacements = { + "\\": r"\textbackslash{}", + "&": r"\&", + "%": r"\%", + "$": r"\$", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\textasciicircum{}", + } + return "".join(replacements.get(ch, ch) for ch in s) + + +def _get(d: Dict[str, Any], path: List[str]) -> Any: + cur: Any = d + for k in path: + if not isinstance(cur, dict) or k not in cur: + return None + cur = cur[k] + return cur + + +def _first_present(d: Dict[str, Any], candidate_paths: List[List[str]]) -> Any: + for p in candidate_paths: + v = _get(d, p) + if v is not None: + return v + return None + + +# --------------------------- +# CLI arg extraction (from JSON) +# --------------------------- + +def _extract_argv(run: Dict[str, Any]) -> Optional[List[str]]: + candidates = [ + ["argv"], + ["commandline"], + ["command_line"], + ["cmdline"], + ["cmd_line"], + ["command"], + ["args"], + ["metadata", "argv"], + ["metadata", "commandline"], + ["metadata", "command_line"], + ["meta", "argv"], + ["meta", "commandline"], + ["meta", "command_line"], + ["header", "argv"], + ["header", "commandline"], + ["header", "command_line"], + ] + val = _first_present(run, candidates) + if isinstance(val, list) and all(isinstance(x, (str, int, float)) for x in val): + return [str(x) for x in val] + if isinstance(val, str): + return val.split() + return None + + +def _extract_params_from_argv(argv: List[str]) -> Dict[str, str]: + params: Dict[str, str] = {} + + def take_value(i: int) -> Optional[str]: + return argv[i + 1] if i + 1 < len(argv) else None + + target = next((tok for tok in argv if isinstance(tok, str) and tok.endswith(".py")), None) + if target: + params["target"] = target + + i = 0 + while i < len(argv): + tok = argv[i] + if tok in ( + "--module-name", + "--num-scens", + "--solver-name", + "--max-iterations", + "--max-solver-threads", + "--default-rho", + "--rel-gap", + "--outfile", + ): + v = take_value(i) + if v is not None: + params[tok.lstrip("-")] = v + i += 2 + continue + if tok in ("--lagrangian", "--xhatshuffle"): + params[tok.lstrip("-")] = "true" + i += 1 + continue + i += 1 + + return params + + +# --------------------------- +# System info collection (Unix/bash assumptions) +# --------------------------- + +def _run_cmd(cmd: List[str]) -> Optional[str]: + try: + p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=False) + out = p.stdout.strip() + return out if out else None + except Exception: + return None + + +def _read_first_existing(paths: List[str]) -> Optional[str]: + for p in paths: + try: + with open(p, "r", encoding="utf-8") as f: + return f.read() + except Exception: + continue + return None + + +def _parse_meminfo_kib(meminfo_text: str) -> Dict[str, int]: + """ + Return selected values from /proc/meminfo in KiB + """ + out: Dict[str, int] = {} + for line in meminfo_text.splitlines(): + m = re.match(r"^(\w+):\s+(\d+)\s+kB\s*$", line) + if m: + out[m.group(1)] = int(m.group(2)) + return out + + +def _format_bytes(n: Optional[int]) -> str: + if n is None: + return "unknown" + # n is bytes + units = ["B", "KiB", "MiB", "GiB", "TiB"] + x = float(n) + i = 0 + while x >= 1024.0 and i < len(units) - 1: + x /= 1024.0 + i += 1 + if i == 0: + return f"{int(x)} {units[i]}" + return f"{x:.2f} {units[i]}" + + +def _collect_system_info() -> Dict[str, str]: + """ + Best-effort system inventory on Unix: + - OS/kernel + - CPU count (logical + physical if available) + - CPU model + - CPU max MHz (or base) + - Total memory and (approx) available memory at report time + + Works on Linux best; degrades gracefully on macOS/others. + """ + info: Dict[str, str] = {} + + # OS / kernel + info["os"] = f"{platform.system()} {platform.release()} ({platform.machine()})" + + # CPU counts + logical = os.cpu_count() + info["cpu_logical"] = str(logical) if logical is not None else "unknown" + + # lscpu (Linux) + lscpu = _run_cmd(["bash", "-lc", "lscpu"]) + cpu_model = None + cpu_mhz = None + cpu_max_mhz = None + cpu_sockets = None + cores_per_socket = None + threads_per_core = None + + if lscpu: + for line in lscpu.splitlines(): + if ":" not in line: + continue + k, v = [x.strip() for x in line.split(":", 1)] + kl = k.lower() + if kl == "model name": + cpu_model = v + elif kl in ("cpu mhz",): + cpu_mhz = v + elif kl in ("cpu max mhz",): + cpu_max_mhz = v + elif kl == "socket(s)": + cpu_sockets = v + elif kl == "core(s) per socket": + cores_per_socket = v + elif kl == "thread(s) per core": + threads_per_core = v + + # sysctl (macOS / BSD) + if cpu_model is None: + cpu_model = _run_cmd(["bash", "-lc", "sysctl -n machdep.cpu.brand_string 2>/dev/null"]) or None + + # Physical cores (best-effort) + physical_cores = None + if cpu_sockets and cores_per_socket: + try: + physical_cores = int(cpu_sockets) * int(cores_per_socket) + except Exception: + physical_cores = None + if physical_cores is None: + # macOS + pc = _run_cmd(["bash", "-lc", "sysctl -n hw.physicalcpu 2>/dev/null"]) + if pc and pc.isdigit(): + physical_cores = int(pc) + + if physical_cores is not None: + info["cpu_physical_cores"] = str(physical_cores) + + if cpu_model: + info["cpu_model"] = cpu_model + + # Frequency + # Prefer max MHz if available + freq = cpu_max_mhz or cpu_mhz + if freq: + info["cpu_mhz"] = freq + + if threads_per_core: + info["threads_per_core"] = threads_per_core + if cpu_sockets: + info["cpu_sockets"] = cpu_sockets + if cores_per_socket: + info["cores_per_socket"] = cores_per_socket + + # Memory: Linux /proc/meminfo or macOS sysctl/vm_stat + meminfo = _read_first_existing(["/proc/meminfo"]) + if meminfo: + m = _parse_meminfo_kib(meminfo) + mem_total_bytes = m.get("MemTotal", 0) * 1024 if "MemTotal" in m else None + mem_avail_bytes = m.get("MemAvailable", 0) * 1024 if "MemAvailable" in m else None + info["mem_total"] = _format_bytes(mem_total_bytes) + info["mem_available"] = _format_bytes(mem_avail_bytes) + else: + # macOS total + mt = _run_cmd(["bash", "-lc", "sysctl -n hw.memsize 2>/dev/null"]) + if mt and mt.isdigit(): + info["mem_total"] = _format_bytes(int(mt)) + # macOS available is trickier; best-effort via vm_stat + vm = _run_cmd(["bash", "-lc", "vm_stat 2>/dev/null"]) + if vm: + # Parse page size and free/inactive/speculative, etc. + page_size = 4096 + mps = re.search(r"page size of (\d+) bytes", vm) + if mps: + page_size = int(mps.group(1)) + counts = {} + for line in vm.splitlines(): + mm = re.match(r"^([^:]+):\s+(\d+)\.", line.strip()) + if mm: + counts[mm.group(1).strip()] = int(mm.group(2)) + # rough estimate: free + inactive + speculative + avail_pages = ( + counts.get("Pages free", 0) + + counts.get("Pages inactive", 0) + + counts.get("Pages speculative", 0) + ) + info["mem_available"] = _format_bytes(avail_pages * page_size) + + return info + + +# --------------------------- +# Scalene view parsing +# --------------------------- + +def _run_scalene_view_cli(json_path: str, reduced: bool, columns: int) -> str: + cmd = ["python", "-m", "scalene", "view", "--cli"] + if reduced: + cmd.append("--reduced") + cmd.append(json_path) + + env = dict(os.environ) + env["COLUMNS"] = str(columns) + + p = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=env, + check=False, + ) + return p.stdout + + +def _parse_cli_percent_totals(cli_text: str) -> Tuple[Optional[float], Optional[float], Optional[float]]: + """ + Parse totals by summing per-line percent columns from Scalene `view --cli` output. + + We match table rows like: + 15 │ 2% │ 24% │ 2% │ ... + + Returns: (python_percent, native_percent, system_percent) + """ + py_pct = 0.0 + nat_pct = 0.0 + sys_pct = 0.0 + saw_any = False + + row_re = re.compile( + r"^\s*\d+\s*│\s*([0-9]+(?:\.[0-9]+)?)?\s*%?\s*│\s*([0-9]+(?:\.[0-9]+)?)?\s*%?\s*│\s*([0-9]+(?:\.[0-9]+)?)?\s*%?\s*│" + ) + + for line in cli_text.splitlines(): + m = row_re.match(line) + if not m: + continue + saw_any = True + a, b, c = m.group(1), m.group(2), m.group(3) + py_pct += float(a) if a else 0.0 + nat_pct += float(b) if b else 0.0 + sys_pct += float(c) if c else 0.0 + + if not saw_any: + return None, None, None + + return py_pct, nat_pct, sys_pct + + +def _fmt(x: Optional[float], digits: int = 2, na: str = r"\textemdash") -> str: + if x is None: + return na + return f"{x:.{digits}f}" + + +# --------------------------- +# Main +# --------------------------- + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--glob", default="scalene_rank_*.json", help="Glob for Scalene JSON files") + ap.add_argument("--out", default="scalene_summary.tex", help="Output LaTeX filename") + ap.add_argument("--caption", default="Scalene timing summary by MPI rank", help="Table caption") + ap.add_argument("--label", default="tab:scalene-summary", help="LaTeX label") + ap.add_argument("--no-totals", action="store_true", help="Do not add totals row") + ap.add_argument("--no-view", action="store_true", help="Do not run `scalene view --cli` (wall only)") + ap.add_argument("--cache-cli", action="store_true", help="Cache CLI output to .cli.txt and reuse") + ap.add_argument("--reduced", action="store_true", help="Pass --reduced to scalene view --cli (recommended)") + ap.add_argument("--columns", type=int, default=200, help="Set COLUMNS for scalene view output (default 200)") + args = ap.parse_args() + + files = sorted(glob.glob(args.glob)) + if not files: + raise SystemExit(f"No files matched glob: {args.glob}") + + # System info + sysinfo = _collect_system_info() + + # Load JSON profiles + runs = [(f, _load_json(f)) for f in files] + + # Use the first JSON file as the "run metadata" source + first_json = runs[0][1] + argv = _extract_argv(first_json) + params: Dict[str, str] = _extract_params_from_argv(argv) if argv else {} + + rows: List[RankRow] = [] + for f, j in runs: + wall = _coerce_float(j.get("elapsed_time_sec")) + + python_s = native_s = system_s = None + + if not args.no_view and wall is not None: + cache_path = f"{f}.cli.txt" + if args.cache_cli and os.path.exists(cache_path): + with open(cache_path, "r", encoding="utf-8") as cf: + cli_text = cf.read() + else: + cli_text = _run_scalene_view_cli(f, reduced=args.reduced, columns=args.columns) + if args.cache_cli: + with open(cache_path, "w", encoding="utf-8") as cf: + cf.write(cli_text) + + py_pct, nat_pct, sys_pct = _parse_cli_percent_totals(cli_text) + if py_pct is not None and nat_pct is not None and sys_pct is not None: + python_s = wall * (py_pct / 100.0) + native_s = wall * (nat_pct / 100.0) + system_s = wall * (sys_pct / 100.0) + + rows.append( + RankRow( + filename=os.path.basename(f), + rank=_parse_rank_from_filename(f), + wall_time=wall, + python_time=python_s, + native_time=native_s, + system_time=system_s, + ) + ) + + rows.sort(key=lambda r: (999999 if r.rank is None else r.rank, r.filename)) + + vals_wall = [r.wall_time for r in rows if r.wall_time is not None] + vals_py = [r.python_time for r in rows if r.python_time is not None] + vals_nat = [r.native_time for r in rows if r.native_time is not None] + vals_sys = [r.system_time for r in rows if r.system_time is not None] + + job_wall = max(vals_wall) if vals_wall else None + sum_py = sum(vals_py) if vals_py else None + sum_nat = sum(vals_nat) if vals_nat else None + sum_sys = sum(vals_sys) if vals_sys else None + + any_time_breakdown = bool(vals_py or vals_nat or vals_sys) + + # Build LaTeX + lines: List[str] = [] + lines.append("% Auto-generated by make_scalene_latex_table.py") + lines.append("") + + # System info block + lines.append(r"\noindent\textbf{System information:}\\") + lines.append(r"\begin{itemize}") + if "os" in sysinfo: + lines.append(rf" \item \texttt{{os}}={{{_latex_escape(sysinfo['os'])}}}") + if "cpu_model" in sysinfo: + lines.append(rf" \item \texttt{{cpu\_model}}={{{_latex_escape(sysinfo['cpu_model'])}}}") + if "cpu_logical" in sysinfo: + lines.append(rf" \item \texttt{{cpu\_logical}}={{{_latex_escape(sysinfo['cpu_logical'])}}}") + if "cpu_physical_cores" in sysinfo: + lines.append(rf" \item \texttt{{cpu\_physical\_cores}}={{{_latex_escape(sysinfo['cpu_physical_cores'])}}}") + if "cpu_mhz" in sysinfo: + lines.append(rf" \item \texttt{{cpu\_mhz}}={{{_latex_escape(sysinfo['cpu_mhz'])}}}") + if "cpu_sockets" in sysinfo: + lines.append(rf" \item \texttt{{cpu\_sockets}}={{{_latex_escape(sysinfo['cpu_sockets'])}}}") + if "cores_per_socket" in sysinfo: + lines.append(rf" \item \texttt{{cores\_per\_socket}}={{{_latex_escape(sysinfo['cores_per_socket'])}}}") + if "threads_per_core" in sysinfo: + lines.append(rf" \item \texttt{{threads\_per\_core}}={{{_latex_escape(sysinfo['threads_per_core'])}}}") + if "mem_total" in sysinfo: + lines.append(rf" \item \texttt{{mem\_total}}={{{_latex_escape(sysinfo['mem_total'])}}}") + if "mem_available" in sysinfo: + lines.append(rf" \item \texttt{{mem\_available}}={{{_latex_escape(sysinfo['mem_available'])}}}") + lines.append(r"\end{itemize}") + lines.append("") + + # Run parameters comment header + lines.append("% Run parameters extracted from JSON (best-effort):") + if argv: + lines.append(f"% argv: {_latex_escape(' '.join(argv))}") + else: + lines.append("% argv: (not found in JSON)") + if params: + lines.append("% Parsed parameters:") + for k in sorted(params.keys()): + lines.append(f"% {k}: {_latex_escape(params[k])}") + lines.append("") + + # Run parameters block + lines.append(r"\noindent\textbf{Run parameters (from Scalene JSON):}\\") + if params: + show_keys = [ + "target", + "module-name", + "num-scens", + "solver-name", + "max-iterations", + "max-solver-threads", + "default-rho", + "rel-gap", + "lagrangian", + "xhatshuffle", + ] + parts = [] + for k in show_keys: + if k in params: + parts.append(rf"\texttt{{{_latex_escape(k)}}}={{{_latex_escape(params[k])}}}") + lines.append(r"\begin{itemize}") + for p in parts: + lines.append(rf" \item {p}") + lines.append(r"\end{itemize}") + else: + lines.append(r"\emph{(Command line not found in JSON.)}\\") + lines.append("") + + if args.no_view: + lines.append( + r"\noindent\emph{Note: Time breakdown requires parsing \texttt{python -m scalene view --cli --reduced }.}" + ) + lines.append("") + elif not any_time_breakdown: + lines.append( + r"\noindent\emph{Note: No per-line time percentages were found in the output of \texttt{scalene view --cli}.}" + ) + lines.append("") + + # Table + lines.append(r"\begin{table}[ht]") + lines.append(r"\centering") + lines.append(r"\begin{tabular}{r l r r r r}") + lines.append(r"\hline") + lines.append(r"Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\") + lines.append(r"\hline") + + for r in rows: + rank_str = "" if r.rank is None else str(r.rank) + lines.append( + rf"{rank_str} & {_latex_escape(r.filename)} & {_fmt(r.wall_time)} & {_fmt(r.python_time)} & {_fmt(r.native_time)} & {_fmt(r.system_time)} \\" + ) + + if not args.no_totals: + lines.append(r"\hline") + lines.append( + rf"\textbf{{Job wall (max)}} & & \textbf{{{_fmt(job_wall)}}} & \textbf{{{_fmt(sum_py)}}} & \textbf{{{_fmt(sum_nat)}}} & \textbf{{{_fmt(sum_sys)}}} \\" + ) + + lines.append(r"\hline") + lines.append(r"\end{tabular}") + lines.append(rf"\caption{{{_latex_escape(args.caption)}}}") + lines.append(rf"\label{{{_latex_escape(args.label)}}}") + lines.append(r"\end{table}") + lines.append("") + + with open(args.out, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + + print(f"Wrote LaTeX to: {args.out}") + print(f"Read {len(files)} JSON files matched by: {args.glob}") + + +if __name__ == "__main__": + main() diff --git a/examples/python_fraction/python_fraction.tex b/examples/python_fraction/python_fraction.tex index 5eed1082f..7f75ae884 100644 --- a/examples/python_fraction/python_fraction.tex +++ b/examples/python_fraction/python_fraction.tex @@ -38,12 +38,277 @@ \maketitle When considering applications in practice, or when comparing to other -packages a question arises concerning the fraction of time that +packages, a question arises concerning the fraction of time that mpi-sppy spends ``in Python'' as opposed to compiled code written in other languages such as C and Fortran. Since solvers, numpy, and MPI are all in the latter category, {\em a priori} one expects that the fraction spent in Python will be small for all but toy problems. +We have use the tool called {\em scalene}, which is designed to +attribute time to Python vs native (it can estimate time spent in +compiled code called from Python, which it calls ``native''). It works +by sampling. + +It turns out that about 10\% to somtimes 20\% of +MPI-SPPY's time is spent in Python code. So you can't get much speed-up just +using another language. Maybe another language will make it easier +for you to do something sophisticated and algorithmic. + +NOTE: I don't yet understand why, based on very limited samples, the fraction of +time spent in python seems to be larger in ranks above the first three. + +\appendix +\section{Dec 2025} + +These experiments use Pyomo models and the Pyomo time is included. + +% Auto-generated by make_scalene_latex_table.py + +\noindent\textbf{System information:}\\ +\begin{itemize} + \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} + \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} + \item \texttt{cpu\_logical}={28} + \item \texttt{cpu\_physical\_cores}={20} + \item \texttt{cpu\_mhz}={5400.0000} + \item \texttt{cpu\_sockets}={1} + \item \texttt{cores\_per\_socket}={20} + \item \texttt{threads\_per\_core}={2} + \item \texttt{mem\_total}={31.03 GiB} + \item \texttt{mem\_available}={25.16 GiB} +\end{itemize} + +% Run parameters extracted from JSON (best-effort): +% argv: ../../mpisppy/generic\_cylinders.py --module-name ../farmer/farmer --num-scens 3 --solver-name gurobi --max-iterations 100 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.0001 +% Parsed parameters: +% default-rho: 1 +% lagrangian: true +% max-iterations: 100 +% max-solver-threads: 4 +% module-name: ../farmer/farmer +% num-scens: 3 +% rel-gap: 0.0001 +% solver-name: gurobi +% target: ../../mpisppy/generic\_cylinders.py +% xhatshuffle: true + +\noindent\textbf{Run parameters (from Scalene JSON):}\\ +\begin{itemize} + \item \texttt{target}={../../mpisppy/generic\_cylinders.py} + \item \texttt{module-name}={../farmer/farmer} + \item \texttt{num-scens}={3} + \item \texttt{solver-name}={gurobi} + \item \texttt{max-iterations}={100} + \item \texttt{max-solver-threads}={4} + \item \texttt{default-rho}={1} + \item \texttt{rel-gap}={0.0001} + \item \texttt{lagrangian}={true} + \item \texttt{xhatshuffle}={true} +\end{itemize} + +\begin{table}[ht] +\centering +\begin{tabular}{r l r r r r} +\hline +Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ +\hline +0 & scalene\_rank\_0.json & 1.90 & 0.17 & 1.54 & 0.11 \\ +1 & scalene\_rank\_1.json & 1.90 & 0.19 & 1.52 & 0.08 \\ +2 & scalene\_rank\_2.json & 1.84 & 0.17 & 1.53 & 0.11 \\ +\hline +\end{tabular} +\caption{Scalene timing summary by MPI rank} +\label{tab:scalene-summary} +\end{table} + +% Auto-generated by make_scalene_latex_table.py + +\noindent\textbf{System information:}\\ +\begin{itemize} + \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} + \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} + \item \texttt{cpu\_logical}={28} + \item \texttt{cpu\_physical\_cores}={20} + \item \texttt{cpu\_mhz}={5400.0000} + \item \texttt{cpu\_sockets}={1} + \item \texttt{cores\_per\_socket}={20} + \item \texttt{threads\_per\_core}={2} + \item \texttt{mem\_total}={31.03 GiB} + \item \texttt{mem\_available}={24.79 GiB} +\end{itemize} + +% Run parameters extracted from JSON (best-effort): +% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 +% Parsed parameters: +% default-rho: 1 +% lagrangian: true +% max-iterations: 10 +% max-solver-threads: 4 +% module-name: ../sslp/sslp +% rel-gap: 0.01 +% solver-name: gurobi +% target: ../../mpisppy/generic\_cylinders.py +% xhatshuffle: true + +\noindent\textbf{Run parameters (from Scalene JSON):}\\ +\begin{itemize} + \item \texttt{target}={../../mpisppy/generic\_cylinders.py} + \item \texttt{module-name}={../sslp/sslp} + \item \texttt{solver-name}={gurobi} + \item \texttt{max-iterations}={10} + \item \texttt{max-solver-threads}={4} + \item \texttt{default-rho}={1} + \item \texttt{rel-gap}={0.01} + \item \texttt{lagrangian}={true} + \item \texttt{xhatshuffle}={true} +\end{itemize} + +\begin{table}[ht] +\centering +\begin{tabular}{r l r r r r} +\hline +Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ +\hline +0 & scalene\_rank\_0.json & 13.83 & 1.24 & 11.20 & 0.83 \\ +1 & scalene\_rank\_1.json & 13.82 & 1.38 & 11.06 & 0.55 \\ +2 & scalene\_rank\_2.json & 13.82 & 1.24 & 11.47 & 0.83 \\ +\hline +\end{tabular} +\caption{Scalene timing summary by MPI rank} +\label{tab:scalene-summary} +\end{table} + +This table shows sslp with more ranks: + +% Auto-generated by make_scalene_latex_table.py + +\noindent\textbf{System information:}\\ +\begin{itemize} + \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} + \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} + \item \texttt{cpu\_logical}={28} + \item \texttt{cpu\_physical\_cores}={20} + \item \texttt{cpu\_mhz}={5400.0000} + \item \texttt{cpu\_sockets}={1} + \item \texttt{cores\_per\_socket}={20} + \item \texttt{threads\_per\_core}={2} + \item \texttt{mem\_total}={31.03 GiB} + \item \texttt{mem\_available}={24.83 GiB} +\end{itemize} + +% Run parameters extracted from JSON (best-effort): +% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 +% Parsed parameters: +% default-rho: 1 +% lagrangian: true +% max-iterations: 10 +% max-solver-threads: 4 +% module-name: ../sslp/sslp +% rel-gap: 0.01 +% solver-name: gurobi +% target: ../../mpisppy/generic\_cylinders.py +% xhatshuffle: true + +\noindent\textbf{Run parameters (from Scalene JSON):}\\ +\begin{itemize} + \item \texttt{target}={../../mpisppy/generic\_cylinders.py} + \item \texttt{module-name}={../sslp/sslp} + \item \texttt{solver-name}={gurobi} + \item \texttt{max-iterations}={10} + \item \texttt{max-solver-threads}={4} + \item \texttt{default-rho}={1} + \item \texttt{rel-gap}={0.01} + \item \texttt{lagrangian}={true} + \item \texttt{xhatshuffle}={true} +\end{itemize} + +\begin{table}[ht] +\centering +\begin{tabular}{r l r r r r} +\hline +Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ +\hline +0 & scalene\_rank\_0.json & 9.89 & 0.89 & 8.01 & 0.59 \\ +1 & scalene\_rank\_1.json & 9.89 & 0.99 & 7.91 & 0.40 \\ +2 & scalene\_rank\_2.json & 9.93 & 0.89 & 8.24 & 0.60 \\ +3 & scalene\_rank\_3.json & 9.89 & 1.88 & 7.02 & 0.00 \\ +4 & scalene\_rank\_4.json & 9.89 & 1.48 & 7.52 & 0.10 \\ +5 & scalene\_rank\_5.json & 9.89 & 1.58 & 7.22 & 0.10 \\ +\hline +\end{tabular} +\caption{Scalene timing summary by MPI rank} +\label{tab:scalene-summary} +\end{table} + +And even more. + +% Auto-generated by make_scalene_latex_table.py + +\noindent\textbf{System information:}\\ +\begin{itemize} + \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} + \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} + \item \texttt{cpu\_logical}={28} + \item \texttt{cpu\_physical\_cores}={20} + \item \texttt{cpu\_mhz}={5400.0000} + \item \texttt{cpu\_sockets}={1} + \item \texttt{cores\_per\_socket}={20} + \item \texttt{threads\_per\_core}={2} + \item \texttt{mem\_total}={31.03 GiB} + \item \texttt{mem\_available}={25.00 GiB} +\end{itemize} + +% Run parameters extracted from JSON (best-effort): +% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 +% Parsed parameters: +% default-rho: 1 +% lagrangian: true +% max-iterations: 10 +% max-solver-threads: 4 +% module-name: ../sslp/sslp +% rel-gap: 0.01 +% solver-name: gurobi +% target: ../../mpisppy/generic\_cylinders.py +% xhatshuffle: true + +\noindent\textbf{Run parameters (from Scalene JSON):}\\ +\begin{itemize} + \item \texttt{target}={../../mpisppy/generic\_cylinders.py} + \item \texttt{module-name}={../sslp/sslp} + \item \texttt{solver-name}={gurobi} + \item \texttt{max-iterations}={10} + \item \texttt{max-solver-threads}={4} + \item \texttt{default-rho}={1} + \item \texttt{rel-gap}={0.01} + \item \texttt{lagrangian}={true} + \item \texttt{xhatshuffle}={true} +\end{itemize} + +\begin{table}[ht] +\centering +\begin{tabular}{r l r r r r} +\hline +Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ +\hline +0 & scalene\_rank\_0.json & 9.44 & 0.85 & 7.65 & 0.57 \\ +1 & scalene\_rank\_1.json & 9.64 & 0.96 & 7.71 & 0.39 \\ +2 & scalene\_rank\_2.json & 9.58 & 0.86 & 7.95 & 0.57 \\ +3 & scalene\_rank\_3.json & 9.73 & 1.85 & 6.91 & 0.00 \\ +4 & scalene\_rank\_4.json & 9.47 & 1.42 & 7.20 & 0.09 \\ +5 & scalene\_rank\_5.json & 9.74 & 1.56 & 7.11 & 0.10 \\ +6 & scalene\_rank\_6.json & 9.78 & 1.76 & 7.33 & 0.20 \\ +7 & scalene\_rank\_7.json & 9.74 & 1.66 & 7.30 & 0.29 \\ +8 & scalene\_rank\_8.json & 9.55 & 1.72 & 6.49 & 0.57 \\ +9 & scalene\_rank\_9.json & 9.71 & 1.94 & 6.99 & 0.19 \\ +10 & scalene\_rank\_10.json & 9.79 & 1.86 & 7.24 & 0.20 \\ +11 & scalene\_rank\_11.json & 9.73 & 1.36 & 7.00 & 0.39 \\ +\hline +\hline +\end{tabular} +\caption{Scalene timing summary by MPI rank} +\label{tab:scalene-summary} +\end{table} + \end{document} diff --git a/examples/python_fraction/readme.rst b/examples/python_fraction/readme.rst new file mode 100644 index 000000000..d6e0e1cc8 --- /dev/null +++ b/examples/python_fraction/readme.rst @@ -0,0 +1,10 @@ +Fraction of time in python +========================== + +See ``python_faction.tex`` for more information. This code suite is probably fragile because +scalene seems to do major updates that change the output format. + +Edit and run a copy of ``tests.sh`` using, e.g. ``$ SOLVER=gurobi mpiexec -np 3 ./tests_ssn.sh`` + +Then edit and run a copy of ``farmer_summary.bash`` + diff --git a/examples/python_fraction/test_sslp.sh b/examples/python_fraction/test_sslp.sh new file mode 100755 index 000000000..3fb5e0264 --- /dev/null +++ b/examples/python_fraction/test_sslp.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# wrap the mpiexec run in python so the rank can be determined +# run with $ SOLVER=gurobi mpiexec -np 3 ./tests.sh +# (do chmod once) + +set -euo pipefail + +SOLVER="${SOLVER:-gurobi}" + +# Determine MPI rank from common env vars (OpenMPI / MPICH / Slurm) +RANK="${OMPI_COMM_WORLD_RANK:-${PMI_RANK:-${SLURM_PROCID:-}}}" +if [[ -z "${RANK}" ]]; then + echo "Could not determine MPI rank from environment (OMPI_COMM_WORLD_RANK / PMI_RANK / SLURM_PROCID)." >&2 + exit 1 +fi + +echo "^^^ sslp_15_45_10 ^^^ rank=${RANK}" + +python -m scalene run \ + --outfile "scalene_rank_${RANK}.txt" \ + ../../mpisppy/generic_cylinders.py \ + --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp_15_45_10 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 + diff --git a/examples/python_fraction/tests.sh b/examples/python_fraction/tests.sh index 7606e526a..6eff173af 100755 --- a/examples/python_fraction/tests.sh +++ b/examples/python_fraction/tests.sh @@ -23,12 +23,12 @@ python -m scalene run \ --module-name ../farmer/farmer \ --num-scens 3 \ --solver-name "${SOLVER}" \ - --max-iterations 10 \ + --max-iterations 100 \ --max-solver-threads 4 \ --default-rho 1 \ --lagrangian \ --xhatshuffle \ - --rel-gap 0.01 + --rel-gap 0.0001 exit diff --git a/examples/sslp/sslp_demo.bash b/examples/sslp/sslp_demo.bash index eae3afa67..7491a786b 100755 --- a/examples/sslp/sslp_demo.bash +++ b/examples/sslp/sslp_demo.bash @@ -4,7 +4,8 @@ # kw_creator, ...), so runs go through mpisppy/generic_cylinders.py. # We assume the current directory is examples/sslp. -SOLVER=xpress_persistent +SOLVER=gurobi +#SOLVER=xpress_persistent #SOLVER=cplex mpiexec -n 11 python -u -m mpi4py ../../mpisppy/generic_cylinders.py --module-name sslp --sslp-data-path=./data/ --solver-name=${SOLVER} --max-solver-threads=1 --default-rho=10.0 --instance-name=sslp_15_45_10 --max-iterations=100 --rel-gap=0.0 --xhatshuffle --presolve --intra-hub-conv-thresh=-0.1 --fwph-objgap-hub --xhatshuffle-rank-ratio=0.1 --sep-rho --surrogate-nonant From d87585f76f865667cdec324519c6b4451954476a Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Sat, 27 Dec 2025 16:57:34 -0800 Subject: [PATCH 03/11] working on the fast/slow cores issue in scalene sampling --- examples/python_fraction/python_fraction.tex | 97 ++++++++++++++++++-- examples/python_fraction/test_sslp.sh | 2 +- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/examples/python_fraction/python_fraction.tex b/examples/python_fraction/python_fraction.tex index 7f75ae884..473c47c2e 100644 --- a/examples/python_fraction/python_fraction.tex +++ b/examples/python_fraction/python_fraction.tex @@ -44,24 +44,26 @@ are all in the latter category, {\em a priori} one expects that the fraction spent in Python will be small for all but toy problems. -We have use the tool called {\em scalene}, which is designed to +We have used the tool called {\em scalene}, which is designed to attribute time to Python vs native (it can estimate time spent in compiled code called from Python, which it calls ``native''). It works by sampling. -It turns out that about 10\% to somtimes 20\% of -MPI-SPPY's time is spent in Python code. So you can't get much speed-up just +It turns out that about 10\% to sometimes 20\% of +MPI-SPPY's time is spent in Python code. The higher fractions +seem to be artifacts of scalene sampling and because of contention and +delays in the solver. + +So you can't get much speed-up just using another language. Maybe another language will make it easier for you to do something sophisticated and algorithmic. -NOTE: I don't yet understand why, based on very limited samples, the fraction of -time spent in python seems to be larger in ranks above the first three. - \appendix \section{Dec 2025} These experiments use Pyomo models and the Pyomo time is included. +\subsection{farmer} % Auto-generated by make_scalene_latex_table.py \noindent\textbf{System information:}\\ @@ -121,8 +123,91 @@ \section{Dec 2025} \label{tab:scalene-summary} \end{table} + +\subsection{sslp runs with forced core placement} + +This is to deal with the fact that my computer has slow cores and fast cores; +the slow cores seem to be worse for the two threads of the +solver causing contention and more samples that appear to be in Python. + +\verb|taskset -c 0-15 mpiexec --report-bindings --bind-to core --map-by slot:PE=2 -np 6 ./test_sslp.sh| + % Auto-generated by make_scalene_latex_table.py +\noindent\textbf{System information:}\\ +\begin{itemize} + \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} + \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} + \item \texttt{cpu\_logical}={28} + \item \texttt{cpu\_physical\_cores}={20} + \item \texttt{cpu\_mhz}={5400.0000} + \item \texttt{cpu\_sockets}={1} + \item \texttt{cores\_per\_socket}={20} + \item \texttt{threads\_per\_core}={2} + \item \texttt{mem\_total}={31.03 GiB} + \item \texttt{mem\_available}={24.33 GiB} +\end{itemize} + +% Run parameters extracted from JSON (best-effort): +% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 2 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 +% Parsed parameters: +% default-rho: 1 +% lagrangian: true +% max-iterations: 10 +% max-solver-threads: 2 +% module-name: ../sslp/sslp +% rel-gap: 0.01 +% solver-name: gurobi +% target: ../../mpisppy/generic\_cylinders.py +% xhatshuffle: true + +\noindent\textbf{Run parameters (from Scalene JSON):}\\ +\begin{itemize} + \item \texttt{target}={../../mpisppy/generic\_cylinders.py} + \item \texttt{module-name}={../sslp/sslp} + \item \texttt{solver-name}={gurobi} + \item \texttt{max-iterations}={10} + \item \texttt{max-solver-threads}={2} + \item \texttt{default-rho}={1} + \item \texttt{rel-gap}={0.01} + \item \texttt{lagrangian}={true} + \item \texttt{xhatshuffle}={true} +\end{itemize} + +\begin{table}[ht] +\centering +\begin{tabular}{r l r r r r} +\hline +Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ +\hline +0 & scalene\_rank\_0.json & 12.41 & 1.12 & 10.05 & 0.74 \\ +1 & scalene\_rank\_1.json & 12.42 & 1.24 & 9.94 & 0.50 \\ +2 & scalene\_rank\_2.json & 12.45 & 1.12 & 10.34 & 0.75 \\ +3 & scalene\_rank\_3.json & 12.45 & 2.37 & 8.84 & 0.00 \\ +4 & scalene\_rank\_4.json & 12.29 & 1.84 & 9.34 & 0.12 \\ +5 & scalene\_rank\_5.json & 12.29 & 1.97 & 8.97 & 0.12 \\ +\hline +\hline +\end{tabular} +\caption{Scalene timing summary by MPI rank for sslp running on just slow cores} +\label{tab:scalene-summary} +\end{table} + +Notice the zero system time on rank 3. This is a sampling/rounding +artifact. I need bigger, longer running instances and maybe a more +homogenous computer. The time that should have been there gets dumped +into the Python bucket. + + + +\subsection{sslp runs with naive core placement by mpi} +% Auto-generated by make_scalene_latex_table.py + +This puts higher ranks on slow cores and others on fast cores, which impacts +the solver and therefore shows more time in Python. + +sslp runs: + \noindent\textbf{System information:}\\ \begin{itemize} \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} diff --git a/examples/python_fraction/test_sslp.sh b/examples/python_fraction/test_sslp.sh index 3fb5e0264..b89c26435 100755 --- a/examples/python_fraction/test_sslp.sh +++ b/examples/python_fraction/test_sslp.sh @@ -20,5 +20,5 @@ echo "^^^ sslp_15_45_10 ^^^ rank=${RANK}" python -m scalene run \ --outfile "scalene_rank_${RANK}.txt" \ ../../mpisppy/generic_cylinders.py \ - --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp_15_45_10 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 + --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp_15_45_10 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 2 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 From 426f8fd029705f7c433ac0e890c8aafad571970f Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Sat, 27 Dec 2025 17:55:14 -0800 Subject: [PATCH 04/11] minor updates --- examples/python_fraction/python_fraction.tex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/python_fraction/python_fraction.tex b/examples/python_fraction/python_fraction.tex index 473c47c2e..83f2e9a41 100644 --- a/examples/python_fraction/python_fraction.tex +++ b/examples/python_fraction/python_fraction.tex @@ -42,14 +42,14 @@ mpi-sppy spends ``in Python'' as opposed to compiled code written in other languages such as C and Fortran. Since solvers, numpy, and MPI are all in the latter category, {\em a priori} one expects that the -fraction spent in Python will be small for all but toy problems. +fraction spent in Python will be small. We have used the tool called {\em scalene}, which is designed to attribute time to Python vs native (it can estimate time spent in compiled code called from Python, which it calls ``native''). It works by sampling. -It turns out that about 10\% to sometimes 20\% of +It turns out that about 10\%, to sometimes 20\%, of MPI-SPPY's time is spent in Python code. The higher fractions seem to be artifacts of scalene sampling and because of contention and delays in the solver. @@ -61,7 +61,7 @@ \appendix \section{Dec 2025} -These experiments use Pyomo models and the Pyomo time is included. +These experiments use Pyomo models and the Pyomo time is included. They are done done a computer with a mixture of core speeds, which causes trouble for scalene. \subsection{farmer} % Auto-generated by make_scalene_latex_table.py From 8f06818e0e50f1b61c982ef60f5118133a3f4ae3 Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Sun, 28 Dec 2025 13:24:53 -0800 Subject: [PATCH 05/11] add a gurobi_persistent run --- examples/python_fraction/python_fraction.tex | 67 +++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/examples/python_fraction/python_fraction.tex b/examples/python_fraction/python_fraction.tex index 83f2e9a41..c93bea1f2 100644 --- a/examples/python_fraction/python_fraction.tex +++ b/examples/python_fraction/python_fraction.tex @@ -206,7 +206,72 @@ \subsection{sslp runs with naive core placement by mpi} This puts higher ranks on slow cores and others on fast cores, which impacts the solver and therefore shows more time in Python. -sslp runs: + +\subsubsection{gurobi\_persistent as the solver for sslp runs} + +% Auto-generated by make_scalene_latex_table.py + +\noindent\textbf{System information:}\\ +\begin{itemize} + \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} + \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} + \item \texttt{cpu\_logical}={28} + \item \texttt{cpu\_physical\_cores}={20} + \item \texttt{cpu\_mhz}={5400.0000} + \item \texttt{cpu\_sockets}={1} + \item \texttt{cores\_per\_socket}={20} + \item \texttt{threads\_per\_core}={2} + \item \texttt{mem\_total}={31.03 GiB} + \item \texttt{mem\_available}={25.40 GiB} +\end{itemize} + +% Run parameters extracted from JSON (best-effort): +% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi\_persistent --max-iterations 10 --max-solver-threads 2 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 +% Parsed parameters: +% default-rho: 1 +% lagrangian: true +% max-iterations: 10 +% max-solver-threads: 2 +% module-name: ../sslp/sslp +% rel-gap: 0.01 +% solver-name: gurobi\_persistent +% target: ../../mpisppy/generic\_cylinders.py +% xhatshuffle: true + +\noindent\textbf{Run parameters (from Scalene JSON):}\\ +\begin{itemize} + \item \texttt{target}={../../mpisppy/generic\_cylinders.py} + \item \texttt{module-name}={../sslp/sslp} + \item \texttt{solver-name}={gurobi\_persistent} + \item \texttt{max-iterations}={10} + \item \texttt{max-solver-threads}={2} + \item \texttt{default-rho}={1} + \item \texttt{rel-gap}={0.01} + \item \texttt{lagrangian}={true} + \item \texttt{xhatshuffle}={true} +\end{itemize} + +\begin{table}[ht] +\centering +\begin{tabular}{r l r r r r} +\hline +Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ +\hline +0 & scalene\_rank\_0.json & 9.72 & 0.87 & 7.87 & 0.58 \\ +1 & scalene\_rank\_1.json & 9.78 & 0.98 & 7.83 & 0.39 \\ +2 & scalene\_rank\_2.json & 9.73 & 0.88 & 8.07 & 0.58 \\ +3 & scalene\_rank\_3.json & 9.73 & 1.85 & 6.91 & 0.00 \\ +4 & scalene\_rank\_4.json & 9.73 & 1.46 & 7.39 & 0.10 \\ +5 & scalene\_rank\_5.json & 9.78 & 1.56 & 7.14 & 0.10 \\ +\hline +\hline +\end{tabular} +\caption{Scalene timing summary by MPI rank, gurob\_persistent} +\label{tab:scalene-summary} +\end{table} + + +\subsubsection{gurobi as the solver for sslp runs} \noindent\textbf{System information:}\\ \begin{itemize} From e1dc796aece8ad884a7dbc345c58bbbfebd41883 Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Wed, 29 Jul 2026 13:33:32 -0700 Subject: [PATCH 06/11] python_fraction: leave examples/sslp/sslp_demo.bash as on main The profiling scripts take SOLVER from the environment, so the demo script's default solver is unrelated to this work. Keeps the branch's net diff confined to examples/python_fraction/. Co-Authored-By: Claude Opus 5 --- examples/sslp/sslp_demo.bash | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/sslp/sslp_demo.bash b/examples/sslp/sslp_demo.bash index 7491a786b..eae3afa67 100755 --- a/examples/sslp/sslp_demo.bash +++ b/examples/sslp/sslp_demo.bash @@ -4,8 +4,7 @@ # kw_creator, ...), so runs go through mpisppy/generic_cylinders.py. # We assume the current directory is examples/sslp. -SOLVER=gurobi -#SOLVER=xpress_persistent +SOLVER=xpress_persistent #SOLVER=cplex mpiexec -n 11 python -u -m mpi4py ../../mpisppy/generic_cylinders.py --module-name sslp --sslp-data-path=./data/ --solver-name=${SOLVER} --max-solver-threads=1 --default-rho=10.0 --instance-name=sslp_15_45_10 --max-iterations=100 --rel-gap=0.0 --xhatshuffle --presolve --intra-hub-conv-thresh=-0.1 --fwph-objgap-hub --xhatshuffle-rank-ratio=0.1 --sep-rho --surrogate-nonant From cfb34f4b1abdde65e840eb92134fb10ea028f06a Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Wed, 29 Jul 2026 13:35:11 -0700 Subject: [PATCH 07/11] python_fraction: add the mpi-sppy copyright header Every non-empty Python file needs the header; test_headers.py was failing on make_scalene_latex_table.py. Co-Authored-By: Claude Opus 5 --- examples/python_fraction/make_scalene_latex_table.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/python_fraction/make_scalene_latex_table.py b/examples/python_fraction/make_scalene_latex_table.py index 90e476b33..a0f34e1ff 100644 --- a/examples/python_fraction/make_scalene_latex_table.py +++ b/examples/python_fraction/make_scalene_latex_table.py @@ -1,4 +1,12 @@ #!/usr/bin/env python3 +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### """ make_scalene_latex_table.py From 37e82b19250bc398fb07afda9d76ffe58c0d20d4 Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Wed, 29 Jul 2026 16:53:09 -0700 Subject: [PATCH 08/11] python_fraction: re-run experiments, add bundles, rewrite the report The measured Python fraction is not a single number: it tracks how much work the solver does per subproblem. The sslp_15_45 instances sit at 8.7-9.6% Python, matching the earlier estimate, while cases whose subproblems are trivial run 58-76% Python. Both regimes attribute essentially all time to spopt.py:337, the Pyomo solve call, which is 6.6% Python / 88.2% native for sslp_15_45_15 but 48.2% / 5.8% for farmer60; the per-solve Python cost is roughly fixed, so it is invisible when the solver is busy and dominant when it is not. Experiments: - Eight cases, three repetitions each, so the tables can show run-to-run spread instead of a single sample. - Bundled cases added, since subproblem size drives the answer. farmer240 vs farmer240_bun10 is a controlled pair. Bundling is not monotone in the Python share: farmer goes 76.1% -> 62.6% but sslp goes 9.6% -> 12.5%, because bundling cut solver work faster than Python work. It won on wall time in both cases. - Unprofiled runs (PROFILE=0) give an overhead column; scalene costs 1.42-1.55x on the Python-heavy cases and 0.99-1.11x on the solver-bound ones, confirming its overhead is Python-side. Tooling: - Read the Python/native/system split out of the scalene JSON instead of scraping `scalene view --cli`. The scraper had stopped working entirely: scalene colourizes even when writing to a pipe, so the row regex matched nothing and every column came out em-dashed. The JSON also avoids --reduced's undercount and the CLI's whole-percent rounding. --from-cli keeps the repaired old path for cross-checking. - Retry a repetition when scalene dies during startup with a KeyError out of importlib, which happens intermittently before any mpi-sppy code runs. - run_experiments.bash / make_tables.bash replace tests.sh, test_sslp.sh and farmer_summary.bash. Note that --solver-name gurobi is Pyomo's file-based interface, which writes an LP file and parses a solution file on every solve; on small subproblems that Python work dominates. The report uses gurobi_persistent throughout and notes the contrast. Co-Authored-By: Claude Opus 5 --- examples/python_fraction/.gitignore | 12 + examples/python_fraction/farmer_summary.bash | 6 - .../make_scalene_latex_table.py | 121 +++- examples/python_fraction/make_tables.bash | 48 ++ examples/python_fraction/python_fraction.tex | 612 ++++++------------ examples/python_fraction/readme.rst | 70 +- examples/python_fraction/run_experiments.bash | 188 ++++++ .../scalene_summary_file_interface.tex | 35 + .../scalene_summary_persistent.tex | 41 ++ examples/python_fraction/scalene_totals.py | 149 +++++ examples/python_fraction/scalene_wrapper.bash | 40 ++ examples/python_fraction/summarize_reps.py | 319 +++++++++ examples/python_fraction/test_sslp.sh | 24 - examples/python_fraction/tests.sh | 72 --- 14 files changed, 1176 insertions(+), 561 deletions(-) create mode 100644 examples/python_fraction/.gitignore delete mode 100644 examples/python_fraction/farmer_summary.bash create mode 100755 examples/python_fraction/make_tables.bash create mode 100755 examples/python_fraction/run_experiments.bash create mode 100644 examples/python_fraction/scalene_summary_file_interface.tex create mode 100644 examples/python_fraction/scalene_summary_persistent.tex create mode 100644 examples/python_fraction/scalene_totals.py create mode 100755 examples/python_fraction/scalene_wrapper.bash create mode 100644 examples/python_fraction/summarize_reps.py delete mode 100755 examples/python_fraction/test_sslp.sh delete mode 100755 examples/python_fraction/tests.sh diff --git a/examples/python_fraction/.gitignore b/examples/python_fraction/.gitignore new file mode 100644 index 000000000..c457b6f1c --- /dev/null +++ b/examples/python_fraction/.gitignore @@ -0,0 +1,12 @@ +# run_experiments.bash writes profiles here by default; they are large +# (several MB per rank per repetition) and are not worth versioning. +results/ + +# Scratch left behind by the cylinders and by LaTeX. +*.log +*.aux +*.out +__pycache__/ + +# Built from python_fraction.tex; not versioned. +*.pdf diff --git a/examples/python_fraction/farmer_summary.bash b/examples/python_fraction/farmer_summary.bash deleted file mode 100644 index 947c27087..000000000 --- a/examples/python_fraction/farmer_summary.bash +++ /dev/null @@ -1,6 +0,0 @@ -python make_scalene_latex_table.py \ - --glob "scalene_rank_*.json" \ - --out scalene_summary_farmer.tex \ - --reduced \ - --cache-cli \ - --columns 200 diff --git a/examples/python_fraction/make_scalene_latex_table.py b/examples/python_fraction/make_scalene_latex_table.py index a0f34e1ff..d76ac8322 100644 --- a/examples/python_fraction/make_scalene_latex_table.py +++ b/examples/python_fraction/make_scalene_latex_table.py @@ -21,26 +21,30 @@ - Run parameters extracted from JSON argv (best-effort) - A per-rank table with: Wall (s): elapsed_time_sec from JSON - Python (s), Native (s), System (s): derived by parsing `scalene view --cli --reduced` - and summing per-line percentage columns (Time Python / native / system), then - multiplying by wall time. + Python (s), Native (s), System (s): wall time times the summed per-line + percentages taken straight out of the JSON (see scalene_totals.py) + Python (%): Python as a percent of the time Scalene attributed to a line Totals row: - Job wall time = max wall time across ranks - - Sum(Python seconds), Sum(Native seconds), Sum(System seconds) across ranks (if available) + - Sum(Python seconds), Sum(Native seconds), Sum(System seconds) across ranks -Why this approach: - With Scalene 2.0.1, `scalene run` emits JSON profiles whose CPU summary fields may be null. - The `scalene view --cli` output contains per-line "% of time" columns; summing those - percentages yields overall percent-of-wall-time totals. +Why read the JSON: + Earlier versions of this script parsed `scalene view --cli --reduced` and summed the + per-line percent columns off the screen. That is fragile: the CLI rounds to whole + percents, --reduced hides low-usage lines so the sums undercount, and the output now + carries ANSI colour codes that broke the row regex outright. The same numbers are in + the JSON at full precision, so that is the default. --from-cli still runs the old path + (with the colour codes stripped) if you want to cross-check the two. Usage: python make_scalene_latex_table.py --glob "scalene_rank_*.json" --out scalene_summary.tex Options: + --from-cli Parse `scalene view --cli` instead of reading the JSON directly --cache-cli Cache CLI output as .cli.txt (reused if present) - --no-view Don't run scalene view (wall time only) - --reduced Pass --reduced to scalene view (recommended) + --no-view Report wall time only, no Python/native/system breakdown + --reduced Pass --reduced to scalene view (only affects --from-cli) --columns N Set terminal width (COLUMNS) for scalene view output (default 200) """ @@ -56,6 +60,8 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple +from scalene_totals import consistency_error, totals_from_json + @dataclass class RankRow: @@ -65,6 +71,8 @@ class RankRow: python_time: Optional[float] native_time: Optional[float] system_time: Optional[float] + python_pct: Optional[float] = None + accounted_pct: Optional[float] = None # --------------------------- @@ -388,6 +396,13 @@ def _run_scalene_view_cli(json_path: str, reduced: bool, columns: int) -> str: return p.stdout +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") + + +def _strip_ansi(s: str) -> str: + return _ANSI_RE.sub("", s) + + def _parse_cli_percent_totals(cli_text: str) -> Tuple[Optional[float], Optional[float], Optional[float]]: """ Parse totals by summing per-line percent columns from Scalene `view --cli` output. @@ -395,6 +410,9 @@ def _parse_cli_percent_totals(cli_text: str) -> Tuple[Optional[float], Optional[ We match table rows like: 15 │ 2% │ 24% │ 2% │ ... + Scalene colourizes this table even when its output is a pipe, so the escape + sequences have to come out before the row pattern will match anything. + Returns: (python_percent, native_percent, system_percent) """ py_pct = 0.0 @@ -407,7 +425,7 @@ def _parse_cli_percent_totals(cli_text: str) -> Tuple[Optional[float], Optional[ ) for line in cli_text.splitlines(): - m = row_re.match(line) + m = row_re.match(_strip_ansi(line)) if not m: continue saw_any = True @@ -439,9 +457,11 @@ def main() -> None: ap.add_argument("--caption", default="Scalene timing summary by MPI rank", help="Table caption") ap.add_argument("--label", default="tab:scalene-summary", help="LaTeX label") ap.add_argument("--no-totals", action="store_true", help="Do not add totals row") - ap.add_argument("--no-view", action="store_true", help="Do not run `scalene view --cli` (wall only)") + ap.add_argument("--no-view", action="store_true", help="Report wall time only, no breakdown") + ap.add_argument("--from-cli", action="store_true", + help="Parse `scalene view --cli` instead of reading the JSON directly") ap.add_argument("--cache-cli", action="store_true", help="Cache CLI output to .cli.txt and reuse") - ap.add_argument("--reduced", action="store_true", help="Pass --reduced to scalene view --cli (recommended)") + ap.add_argument("--reduced", action="store_true", help="Pass --reduced to scalene view --cli (--from-cli only)") ap.add_argument("--columns", type=int, default=200, help="Set COLUMNS for scalene view output (default 200)") args = ap.parse_args() @@ -465,23 +485,39 @@ def main() -> None: wall = _coerce_float(j.get("elapsed_time_sec")) python_s = native_s = system_s = None + python_pct = accounted_pct = None if not args.no_view and wall is not None: - cache_path = f"{f}.cli.txt" - if args.cache_cli and os.path.exists(cache_path): - with open(cache_path, "r", encoding="utf-8") as cf: - cli_text = cf.read() + if args.from_cli: + cache_path = f"{f}.cli.txt" + if args.cache_cli and os.path.exists(cache_path): + with open(cache_path, "r", encoding="utf-8") as cf: + cli_text = cf.read() + else: + cli_text = _run_scalene_view_cli(f, reduced=args.reduced, columns=args.columns) + if args.cache_cli: + with open(cache_path, "w", encoding="utf-8") as cf: + cf.write(cli_text) + + py_pct, nat_pct, sys_pct = _parse_cli_percent_totals(cli_text) + if py_pct is not None and nat_pct is not None and sys_pct is not None: + python_s = wall * (py_pct / 100.0) + native_s = wall * (nat_pct / 100.0) + system_s = wall * (sys_pct / 100.0) + accounted_pct = py_pct + nat_pct + sys_pct + if accounted_pct > 0.0: + python_pct = 100.0 * py_pct / accounted_pct else: - cli_text = _run_scalene_view_cli(f, reduced=args.reduced, columns=args.columns) - if args.cache_cli: - with open(cache_path, "w", encoding="utf-8") as cf: - cf.write(cli_text) - - py_pct, nat_pct, sys_pct = _parse_cli_percent_totals(cli_text) - if py_pct is not None and nat_pct is not None and sys_pct is not None: - python_s = wall * (py_pct / 100.0) - native_s = wall * (nat_pct / 100.0) - system_s = wall * (sys_pct / 100.0) + bad = consistency_error(f) + if bad: + raise SystemExit( + "Scalene JSON failed its internal consistency check, so its " + "layout has probably changed:\n " + bad + ) + t = totals_from_json(f) + python_s, native_s, system_s = t.python_sec, t.native_sec, t.system_sec + python_pct = t.python_fraction + accounted_pct = t.accounted_pct rows.append( RankRow( @@ -491,6 +527,8 @@ def main() -> None: python_time=python_s, native_time=native_s, system_time=system_s, + python_pct=python_pct, + accounted_pct=accounted_pct, ) ) @@ -580,33 +618,50 @@ def main() -> None: if args.no_view: lines.append( - r"\noindent\emph{Note: Time breakdown requires parsing \texttt{python -m scalene view --cli --reduced }.}" + r"\noindent\emph{Note: --no-view was given, so only wall time is reported.}" ) lines.append("") elif not any_time_breakdown: lines.append( - r"\noindent\emph{Note: No per-line time percentages were found in the output of \texttt{scalene view --cli}.}" + r"\noindent\emph{Note: No per-line time percentages were found in the profiles.}" + ) + lines.append("") + + vals_acct = [r.accounted_pct for r in rows if r.accounted_pct is not None] + if vals_acct: + lines.append( + r"\noindent\emph{Scalene attributed " + rf"{min(vals_acct):.1f}--{max(vals_acct):.1f}\% " + r"of wall time to a source line; the Python (\%) column is Python as a " + r"percent of that attributed time.}" ) lines.append("") # Table lines.append(r"\begin{table}[ht]") lines.append(r"\centering") - lines.append(r"\begin{tabular}{r l r r r r}") + lines.append(r"\begin{tabular}{r l r r r r r}") lines.append(r"\hline") - lines.append(r"Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\") + lines.append(r"Rank & File & Wall (s) & Python (s) & Native (s) & System (s) & Python (\%) \\") lines.append(r"\hline") for r in rows: rank_str = "" if r.rank is None else str(r.rank) lines.append( - rf"{rank_str} & {_latex_escape(r.filename)} & {_fmt(r.wall_time)} & {_fmt(r.python_time)} & {_fmt(r.native_time)} & {_fmt(r.system_time)} \\" + rf"{rank_str} & {_latex_escape(r.filename)} & {_fmt(r.wall_time)} & {_fmt(r.python_time)} & {_fmt(r.native_time)} & {_fmt(r.system_time)} & {_fmt(r.python_pct, 1)} \\" ) if not args.no_totals: + # The job-level Python percent is computed from the summed seconds, so + # that ranks are weighted by how long they actually ran. + job_py_pct = None + if sum_py is not None and sum_nat is not None and sum_sys is not None: + denom = sum_py + sum_nat + sum_sys + if denom > 0.0: + job_py_pct = 100.0 * sum_py / denom lines.append(r"\hline") lines.append( - rf"\textbf{{Job wall (max)}} & & \textbf{{{_fmt(job_wall)}}} & \textbf{{{_fmt(sum_py)}}} & \textbf{{{_fmt(sum_nat)}}} & \textbf{{{_fmt(sum_sys)}}} \\" + rf"\textbf{{Job wall (max)}} & & \textbf{{{_fmt(job_wall)}}} & \textbf{{{_fmt(sum_py)}}} & \textbf{{{_fmt(sum_nat)}}} & \textbf{{{_fmt(sum_sys)}}} & \textbf{{{_fmt(job_py_pct, 1)}}} \\" ) lines.append(r"\hline") diff --git a/examples/python_fraction/make_tables.bash b/examples/python_fraction/make_tables.bash new file mode 100755 index 000000000..17d2fb253 --- /dev/null +++ b/examples/python_fraction/make_tables.bash @@ -0,0 +1,48 @@ +#!/bin/bash +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### + +# Regenerate the LaTeX tables in python_fraction.tex from the profiles that +# run_experiments.bash wrote. Cheap to re-run; does not re-run any experiment. +# +# Usage: +# ./make_tables.bash [results_dir] + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RESULTS="${1:-${HERE}/results}" + +# Rank order comes from generic_cylinders: the hub is rank 0 and the spokes +# follow in the order build_spoke_list appends them, which for --lagrangian +# --xhatshuffle is lagrangian then xhatshuffle. +LABELS="PH hub,lagrangian,xhatshuffle" + +CASES=(farmer3 farmer60 farmer240 farmer240_bun10 + sslp_15_45_10 sslp_15_45_15 sslp_15_45_15_bun3 sslp_5_25_50) + +# Primary results: the persistent interface. +python "${HERE}/summarize_reps.py" \ + --results "${RESULTS}" \ + --solvers gurobi_persistent \ + --cases "${CASES[@]}" \ + --rank-labels "${LABELS}" \ + --out "${HERE}/scalene_summary_persistent.tex" + +# Secondary: the file-based interface, for the contrast noted in the writeup. +if [[ -d "${RESULTS}/gurobi" ]]; then + python "${HERE}/summarize_reps.py" \ + --results "${RESULTS}" \ + --solvers gurobi \ + --cases "${CASES[@]}" \ + --rank-labels "${LABELS}" \ + --out "${HERE}/scalene_summary_file_interface.tex" +fi + +echo "Tables written to ${HERE}" diff --git a/examples/python_fraction/python_fraction.tex b/examples/python_fraction/python_fraction.tex index c93bea1f2..bb476cebf 100644 --- a/examples/python_fraction/python_fraction.tex +++ b/examples/python_fraction/python_fraction.tex @@ -49,439 +49,207 @@ compiled code called from Python, which it calls ``native''). It works by sampling. -It turns out that about 10\%, to sometimes 20\%, of -MPI-SPPY's time is spent in Python code. The higher fractions -seem to be artifacts of scalene sampling and because of contention and -delays in the solver. +The short answer is that there is no single number: the fraction +depends almost entirely on how much work the solver does per +subproblem. When the subproblems are substantial, as in the +\texttt{sslp\_15\_45} instances, about 9\% of the time is spent in +Python, which matches the {\em a priori} expectation. When the +subproblems are small enough that the solver returns almost +immediately, as in farmer, the majority of the time is spent in Python +--- 76\% in the most extreme case measured here. The crossover is not +subtle and it is not a sampling artifact. + +The reason is that each subproblem solve costs a roughly fixed amount +of Python work regardless of how hard the subproblem is: the proximal +objective has to be rebuilt and handed to the solver, and the solution +has to be read back. That cost is amortized over the solver's work, so +it is invisible when the solver is busy for a while and dominant when +the solver is not. + +The practical consequences are: -So you can't get much speed-up just -using another language. Maybe another language will make it easier -for you to do something sophisticated and algorithmic. - -\appendix -\section{Dec 2025} - -These experiments use Pyomo models and the Pyomo time is included. They are done done a computer with a mixture of core speeds, which causes trouble for scalene. - -\subsection{farmer} -% Auto-generated by make_scalene_latex_table.py - -\noindent\textbf{System information:}\\ -\begin{itemize} - \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} - \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} - \item \texttt{cpu\_logical}={28} - \item \texttt{cpu\_physical\_cores}={20} - \item \texttt{cpu\_mhz}={5400.0000} - \item \texttt{cpu\_sockets}={1} - \item \texttt{cores\_per\_socket}={20} - \item \texttt{threads\_per\_core}={2} - \item \texttt{mem\_total}={31.03 GiB} - \item \texttt{mem\_available}={25.16 GiB} -\end{itemize} - -% Run parameters extracted from JSON (best-effort): -% argv: ../../mpisppy/generic\_cylinders.py --module-name ../farmer/farmer --num-scens 3 --solver-name gurobi --max-iterations 100 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.0001 -% Parsed parameters: -% default-rho: 1 -% lagrangian: true -% max-iterations: 100 -% max-solver-threads: 4 -% module-name: ../farmer/farmer -% num-scens: 3 -% rel-gap: 0.0001 -% solver-name: gurobi -% target: ../../mpisppy/generic\_cylinders.py -% xhatshuffle: true - -\noindent\textbf{Run parameters (from Scalene JSON):}\\ -\begin{itemize} - \item \texttt{target}={../../mpisppy/generic\_cylinders.py} - \item \texttt{module-name}={../farmer/farmer} - \item \texttt{num-scens}={3} - \item \texttt{solver-name}={gurobi} - \item \texttt{max-iterations}={100} - \item \texttt{max-solver-threads}={4} - \item \texttt{default-rho}={1} - \item \texttt{rel-gap}={0.0001} - \item \texttt{lagrangian}={true} - \item \texttt{xhatshuffle}={true} -\end{itemize} - -\begin{table}[ht] -\centering -\begin{tabular}{r l r r r r} -\hline -Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ -\hline -0 & scalene\_rank\_0.json & 1.90 & 0.17 & 1.54 & 0.11 \\ -1 & scalene\_rank\_1.json & 1.90 & 0.19 & 1.52 & 0.08 \\ -2 & scalene\_rank\_2.json & 1.84 & 0.17 & 1.53 & 0.11 \\ -\hline -\end{tabular} -\caption{Scalene timing summary by MPI rank} -\label{tab:scalene-summary} -\end{table} - - -\subsection{sslp runs with forced core placement} - -This is to deal with the fact that my computer has slow cores and fast cores; -the slow cores seem to be worse for the two threads of the -solver causing contention and more samples that appear to be in Python. - -\verb|taskset -c 0-15 mpiexec --report-bindings --bind-to core --map-by slot:PE=2 -np 6 ./test_sslp.sh| - -% Auto-generated by make_scalene_latex_table.py - -\noindent\textbf{System information:}\\ -\begin{itemize} - \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} - \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} - \item \texttt{cpu\_logical}={28} - \item \texttt{cpu\_physical\_cores}={20} - \item \texttt{cpu\_mhz}={5400.0000} - \item \texttt{cpu\_sockets}={1} - \item \texttt{cores\_per\_socket}={20} - \item \texttt{threads\_per\_core}={2} - \item \texttt{mem\_total}={31.03 GiB} - \item \texttt{mem\_available}={24.33 GiB} -\end{itemize} - -% Run parameters extracted from JSON (best-effort): -% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 2 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 -% Parsed parameters: -% default-rho: 1 -% lagrangian: true -% max-iterations: 10 -% max-solver-threads: 2 -% module-name: ../sslp/sslp -% rel-gap: 0.01 -% solver-name: gurobi -% target: ../../mpisppy/generic\_cylinders.py -% xhatshuffle: true - -\noindent\textbf{Run parameters (from Scalene JSON):}\\ -\begin{itemize} - \item \texttt{target}={../../mpisppy/generic\_cylinders.py} - \item \texttt{module-name}={../sslp/sslp} - \item \texttt{solver-name}={gurobi} - \item \texttt{max-iterations}={10} - \item \texttt{max-solver-threads}={2} - \item \texttt{default-rho}={1} - \item \texttt{rel-gap}={0.01} - \item \texttt{lagrangian}={true} - \item \texttt{xhatshuffle}={true} -\end{itemize} - -\begin{table}[ht] -\centering -\begin{tabular}{r l r r r r} -\hline -Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ -\hline -0 & scalene\_rank\_0.json & 12.41 & 1.12 & 10.05 & 0.74 \\ -1 & scalene\_rank\_1.json & 12.42 & 1.24 & 9.94 & 0.50 \\ -2 & scalene\_rank\_2.json & 12.45 & 1.12 & 10.34 & 0.75 \\ -3 & scalene\_rank\_3.json & 12.45 & 2.37 & 8.84 & 0.00 \\ -4 & scalene\_rank\_4.json & 12.29 & 1.84 & 9.34 & 0.12 \\ -5 & scalene\_rank\_5.json & 12.29 & 1.97 & 8.97 & 0.12 \\ -\hline -\hline -\end{tabular} -\caption{Scalene timing summary by MPI rank for sslp running on just slow cores} -\label{tab:scalene-summary} -\end{table} - -Notice the zero system time on rank 3. This is a sampling/rounding -artifact. I need bigger, longer running instances and maybe a more -homogenous computer. The time that should have been there gets dumped -into the Python bucket. - - - -\subsection{sslp runs with naive core placement by mpi} -% Auto-generated by make_scalene_latex_table.py - -This puts higher ranks on slow cores and others on fast cores, which impacts -the solver and therefore shows more time in Python. - - -\subsubsection{gurobi\_persistent as the solver for sslp runs} - -% Auto-generated by make_scalene_latex_table.py - -\noindent\textbf{System information:}\\ -\begin{itemize} - \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} - \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} - \item \texttt{cpu\_logical}={28} - \item \texttt{cpu\_physical\_cores}={20} - \item \texttt{cpu\_mhz}={5400.0000} - \item \texttt{cpu\_sockets}={1} - \item \texttt{cores\_per\_socket}={20} - \item \texttt{threads\_per\_core}={2} - \item \texttt{mem\_total}={31.03 GiB} - \item \texttt{mem\_available}={25.40 GiB} -\end{itemize} - -% Run parameters extracted from JSON (best-effort): -% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi\_persistent --max-iterations 10 --max-solver-threads 2 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 -% Parsed parameters: -% default-rho: 1 -% lagrangian: true -% max-iterations: 10 -% max-solver-threads: 2 -% module-name: ../sslp/sslp -% rel-gap: 0.01 -% solver-name: gurobi\_persistent -% target: ../../mpisppy/generic\_cylinders.py -% xhatshuffle: true - -\noindent\textbf{Run parameters (from Scalene JSON):}\\ \begin{itemize} - \item \texttt{target}={../../mpisppy/generic\_cylinders.py} - \item \texttt{module-name}={../sslp/sslp} - \item \texttt{solver-name}={gurobi\_persistent} - \item \texttt{max-iterations}={10} - \item \texttt{max-solver-threads}={2} - \item \texttt{default-rho}={1} - \item \texttt{rel-gap}={0.01} - \item \texttt{lagrangian}={true} - \item \texttt{xhatshuffle}={true} + \item If your subproblems keep the solver busy, you cannot get much + speed-up just by using another language. Maybe another language + will make it easier for you to do something sophisticated and + algorithmic. + \item If your subproblems are small, the per-solve Python cost is + what you are paying for, and the fix is to give the solver more + work per call rather than to rewrite anything. Bundling is the + obvious lever, and it is examined below. \end{itemize} -\begin{table}[ht] -\centering -\begin{tabular}{r l r r r r} -\hline -Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ -\hline -0 & scalene\_rank\_0.json & 9.72 & 0.87 & 7.87 & 0.58 \\ -1 & scalene\_rank\_1.json & 9.78 & 0.98 & 7.83 & 0.39 \\ -2 & scalene\_rank\_2.json & 9.73 & 0.88 & 8.07 & 0.58 \\ -3 & scalene\_rank\_3.json & 9.73 & 1.85 & 6.91 & 0.00 \\ -4 & scalene\_rank\_4.json & 9.73 & 1.46 & 7.39 & 0.10 \\ -5 & scalene\_rank\_5.json & 9.78 & 1.56 & 7.14 & 0.10 \\ -\hline -\hline -\end{tabular} -\caption{Scalene timing summary by MPI rank, gurob\_persistent} -\label{tab:scalene-summary} -\end{table} - - -\subsubsection{gurobi as the solver for sslp runs} - -\noindent\textbf{System information:}\\ +\section{Method} + +Every case runs the same three cylinders --- a PH hub, a lagrangian +spoke and an xhatshuffle spoke, one MPI rank each --- so that only the +model and the instance change between cases. The convergence tolerance +is set to zero so that runs end on the iteration limit rather than on a +gap, which keeps the amount of work per case predictable. + +Because scalene samples, every case is run three times and the tables +report the mean over repetitions with the observed min--max range. The +spread turns out to be small for runs of a minute or so (typically a +few tenths of a percentage point) and much larger for the deliberately +short \texttt{farmer3} case, which is why that case is kept: it shows +what the numbers look like when there is not enough run time to sample +properly. + +The numbers are read out of scalene's JSON profile rather than scraped +from the output of \texttt{scalene view --cli}. Summing the per-line +percentages in the JSON gives the same quantities at full precision, +and avoids three problems with scraping the terminal output: it rounds +each line to a whole percent, its \texttt{--reduced} form omits +low-usage lines so the sums undercount, and it is colourized, which +silently broke the row-matching in the earlier version of this code. +Scalene attributes 91--99\% of wall time to some source line; the +percentages below are shares of that attributed time, so the +unattributed remainder is divided out rather than being silently +credited to one of the buckets. + +All runs are on a single machine with homogeneous cores, which avoids a +difficulty that affected an earlier version of these experiments: on a +machine with a mixture of fast and slow cores, ranks landing on slow +cores showed inflated Python fractions. + +\noindent\textbf{System and software:}\\ \begin{itemize} - \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} - \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} - \item \texttt{cpu\_logical}={28} - \item \texttt{cpu\_physical\_cores}={20} - \item \texttt{cpu\_mhz}={5400.0000} - \item \texttt{cpu\_sockets}={1} - \item \texttt{cores\_per\_socket}={20} - \item \texttt{threads\_per\_core}={2} - \item \texttt{mem\_total}={31.03 GiB} - \item \texttt{mem\_available}={24.79 GiB} + \item \texttt{os}={Linux 7.0.0-28-generic (x86\_64)} + \item \texttt{cpu\_model}={AMD Ryzen 7 7840HS} + \item \texttt{cpu\_physical\_cores}={8}, \texttt{cpu\_logical}={16}, + \texttt{threads\_per\_core}={2} + \item \texttt{cpu\_max\_mhz}={5137.9} + \item \texttt{mem\_total}={14.31 GiB} + \item \texttt{python}={3.11.13}, \texttt{scalene}={2.0.1}, + \texttt{pyomo}={6.9.5.dev0}, \texttt{mpi4py}={4.1.0}, + \texttt{gurobipy}={13.0.2} + \item solver interface: \texttt{gurobi\_persistent}, + \texttt{--max-solver-threads 2} \end{itemize} -% Run parameters extracted from JSON (best-effort): -% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 -% Parsed parameters: -% default-rho: 1 -% lagrangian: true -% max-iterations: 10 -% max-solver-threads: 4 -% module-name: ../sslp/sslp -% rel-gap: 0.01 -% solver-name: gurobi -% target: ../../mpisppy/generic\_cylinders.py -% xhatshuffle: true - -\noindent\textbf{Run parameters (from Scalene JSON):}\\ -\begin{itemize} - \item \texttt{target}={../../mpisppy/generic\_cylinders.py} - \item \texttt{module-name}={../sslp/sslp} - \item \texttt{solver-name}={gurobi} - \item \texttt{max-iterations}={10} - \item \texttt{max-solver-threads}={4} - \item \texttt{default-rho}={1} - \item \texttt{rel-gap}={0.01} - \item \texttt{lagrangian}={true} - \item \texttt{xhatshuffle}={true} -\end{itemize} +\section{Results} -\begin{table}[ht] -\centering -\begin{tabular}{r l r r r r} -\hline -Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ -\hline -0 & scalene\_rank\_0.json & 13.83 & 1.24 & 11.20 & 0.83 \\ -1 & scalene\_rank\_1.json & 13.82 & 1.38 & 11.06 & 0.55 \\ -2 & scalene\_rank\_2.json & 13.82 & 1.24 & 11.47 & 0.83 \\ -\hline -\end{tabular} -\caption{Scalene timing summary by MPI rank} -\label{tab:scalene-summary} -\end{table} - -This table shows sslp with more ranks: - -% Auto-generated by make_scalene_latex_table.py - -\noindent\textbf{System information:}\\ -\begin{itemize} - \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} - \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} - \item \texttt{cpu\_logical}={28} - \item \texttt{cpu\_physical\_cores}={20} - \item \texttt{cpu\_mhz}={5400.0000} - \item \texttt{cpu\_sockets}={1} - \item \texttt{cores\_per\_socket}={20} - \item \texttt{threads\_per\_core}={2} - \item \texttt{mem\_total}={31.03 GiB} - \item \texttt{mem\_available}={24.83 GiB} -\end{itemize} - -% Run parameters extracted from JSON (best-effort): -% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 -% Parsed parameters: -% default-rho: 1 -% lagrangian: true -% max-iterations: 10 -% max-solver-threads: 4 -% module-name: ../sslp/sslp -% rel-gap: 0.01 -% solver-name: gurobi -% target: ../../mpisppy/generic\_cylinders.py -% xhatshuffle: true - -\noindent\textbf{Run parameters (from Scalene JSON):}\\ -\begin{itemize} - \item \texttt{target}={../../mpisppy/generic\_cylinders.py} - \item \texttt{module-name}={../sslp/sslp} - \item \texttt{solver-name}={gurobi} - \item \texttt{max-iterations}={10} - \item \texttt{max-solver-threads}={4} - \item \texttt{default-rho}={1} - \item \texttt{rel-gap}={0.01} - \item \texttt{lagrangian}={true} - \item \texttt{xhatshuffle}={true} -\end{itemize} - -\begin{table}[ht] -\centering -\begin{tabular}{r l r r r r} -\hline -Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ -\hline -0 & scalene\_rank\_0.json & 9.89 & 0.89 & 8.01 & 0.59 \\ -1 & scalene\_rank\_1.json & 9.89 & 0.99 & 7.91 & 0.40 \\ -2 & scalene\_rank\_2.json & 9.93 & 0.89 & 8.24 & 0.60 \\ -3 & scalene\_rank\_3.json & 9.89 & 1.88 & 7.02 & 0.00 \\ -4 & scalene\_rank\_4.json & 9.89 & 1.48 & 7.52 & 0.10 \\ -5 & scalene\_rank\_5.json & 9.89 & 1.58 & 7.22 & 0.10 \\ -\hline -\end{tabular} -\caption{Scalene timing summary by MPI rank} -\label{tab:scalene-summary} -\end{table} - -And even more. - -% Auto-generated by make_scalene_latex_table.py - -\noindent\textbf{System information:}\\ -\begin{itemize} - \item \texttt{os}={Linux 6.14.0-37-generic (x86\_64)} - \item \texttt{cpu\_model}={Intel(R) Core(TM) i7-14700} - \item \texttt{cpu\_logical}={28} - \item \texttt{cpu\_physical\_cores}={20} - \item \texttt{cpu\_mhz}={5400.0000} - \item \texttt{cpu\_sockets}={1} - \item \texttt{cores\_per\_socket}={20} - \item \texttt{threads\_per\_core}={2} - \item \texttt{mem\_total}={31.03 GiB} - \item \texttt{mem\_available}={25.00 GiB} -\end{itemize} +Table~\ref{tab:python-fraction-summary} gives the split by case and +Table~\ref{tab:python-fraction-by-rank} breaks the Python percentage +out by cylinder. The cases are: -% Run parameters extracted from JSON (best-effort): -% argv: ../../mpisppy/generic\_cylinders.py --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp\_15\_45\_10 --solver-name gurobi --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 -% Parsed parameters: -% default-rho: 1 -% lagrangian: true -% max-iterations: 10 -% max-solver-threads: 4 -% module-name: ../sslp/sslp -% rel-gap: 0.01 -% solver-name: gurobi -% target: ../../mpisppy/generic\_cylinders.py -% xhatshuffle: true - -\noindent\textbf{Run parameters (from Scalene JSON):}\\ \begin{itemize} - \item \texttt{target}={../../mpisppy/generic\_cylinders.py} - \item \texttt{module-name}={../sslp/sslp} - \item \texttt{solver-name}={gurobi} - \item \texttt{max-iterations}={10} - \item \texttt{max-solver-threads}={4} - \item \texttt{default-rho}={1} - \item \texttt{rel-gap}={0.01} - \item \texttt{lagrangian}={true} - \item \texttt{xhatshuffle}={true} + \item \texttt{farmer3} --- 3 scenarios, 100 iterations. Deliberately + short: about two seconds unprofiled, five under scalene. + \item \texttt{farmer60}, \texttt{farmer240} --- 60 and 240 + scenarios, 400 and 200 iterations. Many trivially small LP + subproblems. + \item \texttt{farmer240\_bun10} --- \texttt{farmer240} with ten + scenarios gathered into each proper bundle, so 24 subproblems + instead of 240. + \item \texttt{sslp\_15\_45\_10}, \texttt{sslp\_15\_45\_15} --- 10 and + 15 scenarios, 50 iterations. MIP subproblems that keep the solver + busy. + \item \texttt{sslp\_15\_45\_15\_bun3} --- + \texttt{sslp\_15\_45\_15} with three scenarios per bundle, so 5 + subproblems instead of 15. + \item \texttt{sslp\_5\_25\_50} --- 50 scenarios, 150 iterations. Many + small MIP subproblems. \end{itemize} -\begin{table}[ht] -\centering -\begin{tabular}{r l r r r r} -\hline -Rank & File & Wall (s) & Python (s) & Native (s) & System (s) \\ -\hline -0 & scalene\_rank\_0.json & 9.44 & 0.85 & 7.65 & 0.57 \\ -1 & scalene\_rank\_1.json & 9.64 & 0.96 & 7.71 & 0.39 \\ -2 & scalene\_rank\_2.json & 9.58 & 0.86 & 7.95 & 0.57 \\ -3 & scalene\_rank\_3.json & 9.73 & 1.85 & 6.91 & 0.00 \\ -4 & scalene\_rank\_4.json & 9.47 & 1.42 & 7.20 & 0.09 \\ -5 & scalene\_rank\_5.json & 9.74 & 1.56 & 7.11 & 0.10 \\ -6 & scalene\_rank\_6.json & 9.78 & 1.76 & 7.33 & 0.20 \\ -7 & scalene\_rank\_7.json & 9.74 & 1.66 & 7.30 & 0.29 \\ -8 & scalene\_rank\_8.json & 9.55 & 1.72 & 6.49 & 0.57 \\ -9 & scalene\_rank\_9.json & 9.71 & 1.94 & 6.99 & 0.19 \\ -10 & scalene\_rank\_10.json & 9.79 & 1.86 & 7.24 & 0.20 \\ -11 & scalene\_rank\_11.json & 9.73 & 1.36 & 7.00 & 0.39 \\ -\hline -\hline -\end{tabular} -\caption{Scalene timing summary by MPI rank} -\label{tab:scalene-summary} -\end{table} - +\input{scalene_summary_persistent} + +The ordering is by subproblem difficulty, not by instance size. The two +\texttt{sslp\_15\_45} cases sit at 8.7\% and 9.6\% Python; every case +whose subproblems are small sits far above that, from 58\% for +\texttt{sslp\_5\_25\_50} up to 76\% for \texttt{farmer240}. Note that +\texttt{sslp\_5\_25\_50} has more scenarios than +\texttt{sslp\_15\_45\_15} and yet spends six times the fraction of its +time in Python: what matters is the size of each subproblem, not how +many there are. + +Looking at where the samples land makes the mechanism explicit. In both +regimes essentially all of the time is attributed to one line, +\texttt{spopt.py:337}, which is the Pyomo \texttt{solve} call. For +\texttt{sslp\_15\_45\_15} that line is 6.6\% Python and 88.2\% native +--- the solver is doing the work. For \texttt{farmer60} the same line +is 48.2\% Python and 5.8\% native, and a further 9.4\% of the run is +Python time in \texttt{spopt.py:267}, where the proximal objective is +handed to the solver on each iteration. That objective work scales with +the size of the model rather than with the difficulty of the solve, +which is exactly why it is invisible for sslp and dominant for farmer. + +The per-cylinder table shows the PH hub and the lagrangian spoke +agreeing closely within every case, as they should, since they solve +the same subproblems with different objectives. The xhatshuffle spoke +is consistently the most Python-heavy of the three, and the gap widens +as the hub's own Python share falls: 6.8\% against 13.0\% for +\texttt{sslp\_15\_45\_10}, and 5.1\% against 25.3\% for +\texttt{sslp\_15\_45\_15\_bun3}. This is expected rather than +anomalous. The xhatshuffle spoke is looking for a feasible incumbent +rather than solving the subproblem to optimality, so much of its work +is Python bookkeeping --- shuffling the scenario order, fixing +candidate nonanticipative values, and checking the result --- with +comparatively little time left inside the solver. It is a small +fraction of total work, since it is one rank of three, but it is worth +knowing that the cylinders are not interchangeable for this +measurement. + +\subsection{Bundles} + +Bundling gives the solver more work per call, so it is the natural +response to a high Python fraction. It helps, but not reliably, and it +is worth being clear about why. + +For farmer, bundling ten scenarios per subproblem takes the Python +share from 76.1\% down to 62.6\% and cuts wall time by a factor of +four, from 82.9 to 20.9 seconds. Fewer, larger solves means fewer +round trips through Pyomo, and the wall-clock gain is large. The Python +share nonetheless stays high, because a bundle of ten farmer scenarios +is still a trivial LP. + +For \texttt{sslp\_15\_45\_15}, bundling three scenarios per subproblem +moves the Python share the other way, from 9.6\% up to 12.5\%, while +also cutting wall time from 104.8 to 73.7 seconds. Both numbers moved +in the direction that makes sense once the effect is separated into its +two parts: bundling reduced the total solver work more than it reduced +the total Python work, so Python's {\em share} rose even though the run +got faster. + +So bundling should be judged on wall time, where it won in both cases, +and not on the Python fraction, which it can move either way. A high +Python fraction is a symptom worth investigating, but driving it down +is not itself the objective. + +\subsection{Caveats} + +Scalene's instrumentation is not free, and its cost falls mostly on +Python, so these Python percentages are upper bounds. The overhead +column in Table~\ref{tab:python-fraction-summary} is the ratio of +profiled to unprofiled wall time for the same case, and it lines up +with the Python column: the solver-bound sslp cases run at +0.99--1.11$\times$, essentially unaffected, while the Python-heavy +farmer cases run at 1.42--1.55$\times$. So the true Python fractions +for the farmer cases are somewhat lower than the table reports. The +exception is \texttt{farmer3} at 2.36$\times$, which is not a +Python-share effect at all: the profiler's fixed startup cost is simply +large next to a two-second run, which is another reason not to trust +that row for anything but its variability. + +The gap between the two regimes is far too large to be an artifact of +this overhead --- 9\% against 76\% will not be closed by a factor of +1.5 --- but the individual percentages should be read as approximate. + +Two further limits are worth stating. These are Pyomo models and the +Pyomo time is included in the Python total, so a good deal of what is +labelled Python here is Pyomo rather than mpi-sppy. And all of this is +one machine, one solver, and three repetitions per case. + +\subsection{A note on non-persistent solvers} + +All of the above uses \texttt{gurobi\_persistent}. If you use a +non-persistent interface instead --- \texttt{--solver-name gurobi} is +Pyomo's file-based interface, which writes an LP file and parses a +solution file on every solve --- then Pyomo does a great deal of extra +Python work per solve, which matters most when the subproblems are +small: \texttt{sslp\_5\_25\_50} goes from 58.1\% to 75.4\% Python and +from 74 to 124 seconds, and even the solver-bound +\texttt{sslp\_15\_45\_10}, whose wall time barely moves, goes from +8.7\% to 31.8\% Python. Use a persistent interface if you have one. \end{document} - -scalene is designed to attribute time to Python vs native (it can estimate time spent in compiled code called from Python). It’s often the most straightforward way to get exactly what you asked. - -Run: - -python -m pip install scalene -python -m scalene your_script.py [args...] - - -It reports per-file and per-line: - -Python time - -Native time (extensions, libraries like NumPy, solver bindings, etc.) - -MPI note: if you run under mpiexec, you’ll get output per rank (may need to direct to separate files): - -mpiexec -np 4 python -m scalene --outfile scalene_rank_%r.txt your_script.py ... - - -If %r isn’t supported in your shell, just set unique outfile names using env vars per rank. - -This is usually the quickest route to “fraction spent in Python.” - diff --git a/examples/python_fraction/readme.rst b/examples/python_fraction/readme.rst index d6e0e1cc8..8f250dc53 100644 --- a/examples/python_fraction/readme.rst +++ b/examples/python_fraction/readme.rst @@ -1,10 +1,72 @@ Fraction of time in python ========================== -See ``python_faction.tex`` for more information. This code suite is probably fragile because -scalene seems to do major updates that change the output format. +See ``python_fraction.tex`` for the writeup and the results. -Edit and run a copy of ``tests.sh`` using, e.g. ``$ SOLVER=gurobi mpiexec -np 3 ./tests_ssn.sh`` +Files +----- -Then edit and run a copy of ``farmer_summary.bash`` +``run_experiments.bash`` + Runs every case under scalene, repeating each case so that the writeup can + show run-to-run spread instead of a single sample. Repetitions matter here: + scalene works by sampling, so one run of one case is weak evidence. +``scalene_wrapper.bash`` + Rank-aware scalene launcher; ``mpiexec`` runs this rather than python + directly, so that each rank can name its own output file. Not normally run + by hand. + +``summarize_reps.py`` + Aggregates the repetitions into the LaTeX tables. + +``make_tables.bash`` + Regenerates the tables from profiles already on disk. No experiments rerun. + +``make_scalene_latex_table.py`` + Detail table for a single run (one directory of per-rank profiles). + +``scalene_totals.py`` + Pulls the Python/native/system split out of a scalene JSON profile. Shared + by the two table generators. + +Running +------- + +Profile the default case list with the persistent solver interface, three +repetitions each:: + + $ SOLVER=gurobi_persistent ./run_experiments.bash + +Then, to get the unprofiled wall times that the overhead column needs:: + + $ SOLVER=gurobi_persistent PROFILE=0 ./run_experiments.bash + +And regenerate the tables:: + + $ ./make_tables.bash + +Individual cases can be named on the command line, e.g. +``./run_experiments.bash farmer60 sslp_5_25_50``. + +Notes +----- + +Prefer a persistent solver interface. ``--solver-name gurobi`` is Pyomo's +file-based interface, which writes an LP file and parses a solution file in +Python on every solve; on small subproblems that Python work dominates +everything else, and the measured Python fraction then says more about Pyomo's +file writer than about mpi-sppy. + +The numbers come from the scalene JSON rather than from scraping +``scalene view --cli``. Reading the JSON avoids three problems with scraping: +the CLI rounds each line to a whole percent, ``--reduced`` hides low-usage +lines so the sums undercount, and the output is colourized, which silently +broke the original row-matching regex. ``make_scalene_latex_table.py --from-cli`` +still parses the CLI if you want to compare the two paths. + +Scalene occasionally dies during startup with a ``KeyError`` from inside +``importlib``, before any mpi-sppy code runs; ``run_experiments.bash`` retries a +repetition that comes back with fewer profiles than ranks. + +This code suite is probably fragile because scalene seems to do major updates +that change the output format. It was last run against scalene 2.0.1. diff --git a/examples/python_fraction/run_experiments.bash b/examples/python_fraction/run_experiments.bash new file mode 100755 index 000000000..c17de5e54 --- /dev/null +++ b/examples/python_fraction/run_experiments.bash @@ -0,0 +1,188 @@ +#!/bin/bash +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### + +# Run the "fraction of time in Python" experiments under scalene. +# +# Every case runs the same three cylinders (PH hub, lagrangian, xhatshuffle) so +# that only the model and the instance size change. Each case is repeated REPS +# times so that the report can show run-to-run spread rather than a single +# sample; scalene works by sampling, so a single run says very little. +# +# Usage: +# ./run_experiments.bash # all cases, REPS reps each +# REPS=1 ./run_experiments.bash farmer3 # just one case, one rep +# +# Environment: +# SOLVER solver name (default gurobi) +# REPS repetitions per case (default 3) +# NP number of MPI ranks, i.e. cylinders (default 3) +# THREADS --max-solver-threads (default 2) +# RESULTS output directory (default ./results) +# TRIES attempts per rep before giving up (default 3); see the retry note below +# PROFILE 1 (default) to run under scalene; 0 to run the identical cases with +# no profiler and record only wall time, in /wall.txt. The +# unprofiled times are what make it possible to state how much of the +# measured Python time is scalene's own instrumentation overhead. +# +# The solver name is part of the output path, because which Pyomo solver +# interface is used turns out to dominate the answer: "gurobi" is the +# file-based interface, which writes an LP file and parses a solution file in +# Python, while "gurobi_persistent" keeps the model in the solver through its C +# API. Run the sweep once per interface and compare. +# +# Profiles land in $RESULTS///rep/scalene_rank_.json +# Then run ./make_tables.bash to regenerate the LaTeX. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "${HERE}/../.." && pwd)" +EXAMPLES="${REPO}/examples" +DRIVER="${REPO}/mpisppy/generic_cylinders.py" + +SOLVER="${SOLVER:-gurobi}" +REPS="${REPS:-3}" +NP="${NP:-3}" +THREADS="${THREADS:-2}" +RESULTS="${RESULTS:-${HERE}/results}" +TRIES="${TRIES:-3}" +PROFILE="${PROFILE:-1}" + +# Cylinders and PH settings shared by every case. rel-gap is 0 so that runs end +# on max-iterations (or exact convergence) instead of on a gap tolerance, which +# keeps the amount of work per case predictable. +COMMON=(--solver-name "${SOLVER}" + --max-solver-threads "${THREADS}" + --default-rho 1 + --lagrangian + --xhatshuffle + --rel-gap 0.0) + +# Case definitions. Iteration counts were calibrated so that the "long" cases +# each take roughly a minute of wall time; farmer3 is deliberately left short +# because the short-run numbers are themselves a finding. +# +# The bundled cases exist because subproblem size turns out to drive the answer. +# farmer240 and farmer240_bun10 are a controlled pair: same instance, same +# iteration count, differing only in whether ten scenarios are gathered into one +# subproblem. sslp_15_45_15_bun3 is the same comparison on a MIP. +case_args() { + case "$1" in + farmer3) + echo "--module-name ${EXAMPLES}/farmer/farmer --num-scens 3 --max-iterations 100" + ;; + farmer60) + echo "--module-name ${EXAMPLES}/farmer/farmer --num-scens 60 --max-iterations 400" + ;; + farmer240) + echo "--module-name ${EXAMPLES}/farmer/farmer --num-scens 240 --max-iterations 200" + ;; + farmer240_bun10) + echo "--module-name ${EXAMPLES}/farmer/farmer --num-scens 240 --scenarios-per-bundle 10 --max-iterations 200" + ;; + sslp_15_45_10) + echo "--module-name ${EXAMPLES}/sslp/sslp --sslp-data-path ${EXAMPLES}/sslp/data --instance-name sslp_15_45_10 --max-iterations 50" + ;; + sslp_15_45_15) + echo "--module-name ${EXAMPLES}/sslp/sslp --sslp-data-path ${EXAMPLES}/sslp/data --instance-name sslp_15_45_15 --max-iterations 50" + ;; + sslp_15_45_15_bun3) + echo "--module-name ${EXAMPLES}/sslp/sslp --sslp-data-path ${EXAMPLES}/sslp/data --instance-name sslp_15_45_15 --scenarios-per-bundle 3 --max-iterations 50" + ;; + sslp_5_25_50) + echo "--module-name ${EXAMPLES}/sslp/sslp --sslp-data-path ${EXAMPLES}/sslp/data --instance-name sslp_5_25_50 --max-iterations 150" + ;; + *) + echo "unknown case: $1" >&2 + return 1 + ;; + esac +} + +ALL_CASES=(farmer3 farmer60 farmer240 farmer240_bun10 + sslp_15_45_10 sslp_15_45_15 sslp_15_45_15_bun3 sslp_5_25_50) +CASES=("${@:-}") +if [[ -z "${CASES[0]}" ]]; then + CASES=("${ALL_CASES[@]}") +fi + +echo "solver=${SOLVER} ranks=${NP} threads=${THREADS} reps=${REPS}" +echo "results -> ${RESULTS}" + +# Scalene instruments the import machinery, and occasionally a rank dies during +# startup with a KeyError out of importlib's lock handling before any mpi-sppy +# code runs. That is a profiler startup race, not a property of the case, so a +# rep that comes back with fewer than NP profiles is simply run again. Failures +# are counted and reported at the end so a retried-away problem is still +# visible; a rep that never succeeds is left out of the results rather than +# silently reported as if it had worked. +failures=0 +retries=0 + +# Unprofiled runs go somewhere else so that they cannot overwrite the profiles. +SUBDIR="${SOLVER}" +if [[ "${PROFILE}" == "0" ]]; then + SUBDIR="${SOLVER}-unprofiled" +fi + +for c in "${CASES[@]}"; do + read -r -a cargs <<< "$(case_args "$c")" + for rep in $(seq 1 "${REPS}"); do + outdir="${RESULTS}/${SUBDIR}/${c}/rep${rep}" + echo "^^^ ${c} rep ${rep} ^^^" + for try in $(seq 1 "${TRIES}"); do + rm -rf "${outdir}" + mkdir -p "${outdir}" + if [[ "${PROFILE}" == "0" ]]; then + # Same case, no profiler: record wall time only. + /usr/bin/time -f "%e" -o "${outdir}/wall.txt" \ + mpiexec --oversubscribe -np "${NP}" \ + python -m mpi4py "${DRIVER}" "${cargs[@]}" "${COMMON[@]}" \ + > "${outdir}/run.log" 2>&1 || true + if grep -q 'Cylinder finalization complete' "${outdir}/run.log"; then + n_found="${NP}" + echo " wall $(cat "${outdir}/wall.txt")s (unprofiled)" + break + fi + n_found=0 + else + OUTDIR="${outdir}" mpiexec --oversubscribe -np "${NP}" \ + "${HERE}/scalene_wrapper.bash" \ + "${DRIVER}" "${cargs[@]}" "${COMMON[@]}" \ + > "${outdir}/run.log" 2>&1 || true + n_found=$(find "${outdir}" -name 'scalene_rank_*.json' | wc -l) + if [[ "${n_found}" -eq "${NP}" ]]; then + break + fi + fi + if [[ "${PROFILE}" == "0" ]]; then + echo " attempt ${try}/${TRIES}: run did not complete" \ + "(see ${outdir}/run.log)" >&2 + else + echo " attempt ${try}/${TRIES}: expected ${NP} profiles, found ${n_found}" \ + "(see ${outdir}/run.log)" >&2 + fi + retries=$((retries + 1)) + done + if [[ "${n_found}" -ne "${NP}" ]]; then + echo " GIVING UP on ${c} rep ${rep} after ${TRIES} attempts" >&2 + failures=$((failures + 1)) + continue + fi + if [[ "${PROFILE}" != "0" ]]; then + tail -1 "${outdir}/run.log" + fi + done +done + +echo "done: ${retries} retried attempt(s), ${failures} rep(s) abandoned" +if [[ "${failures}" -ne 0 ]]; then + exit 1 +fi diff --git a/examples/python_fraction/scalene_summary_file_interface.tex b/examples/python_fraction/scalene_summary_file_interface.tex new file mode 100644 index 000000000..4f589362a --- /dev/null +++ b/examples/python_fraction/scalene_summary_file_interface.tex @@ -0,0 +1,35 @@ +% Auto-generated by summarize_reps.py -- do not edit by hand + +\begin{table}[ht] +\centering +\begin{tabular}{l r r r r r} +\hline +Case & Reps & Wall (s) & Python (\%) & Native (\%) & System (\%) \\ +\hline +farmer3 & 3 & 5.8 & 19.8 (18.5--21.5) & 68.1 & 12.1 \\ +farmer60 & 3 & 72.1 & 73.9 (73.1--74.3) & 16.7 & 9.4 \\ +sslp\_15\_45\_10 & 3 & 72.2 & 31.8 (31.7--32.0) & 66.7 & 1.5 \\ +sslp\_15\_45\_15 & 3 & 115.3 & 33.4 (33.3--33.5) & 65.2 & 1.4 \\ +sslp\_5\_25\_50 & 3 & 124.3 & 75.4 (75.3--75.5) & 20.7 & 3.9 \\ +\hline +\end{tabular} +\caption{Time split by case, averaged over repetitions, with the min--max range over repetitions shown for the Python percentage. Percentages are of the time Scalene attributed to a source line (95.1--99.0\% of wall time here); wall time is the maximum over ranks.} +\label{tab:python-fraction-summary} +\end{table} + +\begin{table}[ht] +\centering +\begin{tabular}{l r r r} +\hline +Case & PH hub & lagrangian & xhatshuffle \\ +\hline +farmer3 & 18.3 (17.8--18.8) & 21.6 (18.6--25.2) & 19.5 (18.6--20.3) \\ +farmer60 & 71.4 (70.2--72.3) & 76.5 (75.9--77.0) & 73.7 (73.4--74.1) \\ +sslp\_15\_45\_10 & 33.0 (32.9--33.2) & 32.6 (32.3--32.8) & 29.8 (29.5--30.0) \\ +sslp\_15\_45\_15 & 27.0 (26.9--27.0) & 36.3 (36.1--36.4) & 37.1 (36.9--37.3) \\ +sslp\_5\_25\_50 & 72.2 (72.1--72.4) & 73.3 (73.1--73.5) & 80.7 (80.5--81.1) \\ +\hline +\end{tabular} +\caption{Python percentage by cylinder: mean over repetitions with the min--max range in parentheses.} +\label{tab:python-fraction-by-rank} +\end{table} diff --git a/examples/python_fraction/scalene_summary_persistent.tex b/examples/python_fraction/scalene_summary_persistent.tex new file mode 100644 index 000000000..13e03b9c5 --- /dev/null +++ b/examples/python_fraction/scalene_summary_persistent.tex @@ -0,0 +1,41 @@ +% Auto-generated by summarize_reps.py -- do not edit by hand + +\begin{table}[ht] +\centering +\begin{tabular}{l r r r r r r} +\hline +Case & Reps & Wall (s) & Python (\%) & Native (\%) & System (\%) & Overhead \\ +\hline +farmer3 & 3 & 5.3 & 16.2 (14.4--17.6) & 70.8 & 13.0 & 2.36$\times$ \\ +farmer60 & 3 & 43.4 & 71.3 (70.3--72.4) & 16.2 & 12.4 & 1.55$\times$ \\ +farmer240 & 3 & 82.9 & 76.1 (75.2--77.0) & 11.8 & 12.2 & 1.42$\times$ \\ +farmer240\_bun10 & 3 & 20.9 & 62.6 (61.9--63.2) & 28.6 & 8.8 & 1.48$\times$ \\ +sslp\_15\_45\_10 & 3 & 74.0 & 8.7 (8.5--9.0) & 89.3 & 2.0 & 1.09$\times$ \\ +sslp\_15\_45\_15 & 3 & 104.8 & 9.6 (9.4--9.9) & 88.5 & 1.9 & 1.11$\times$ \\ +sslp\_15\_45\_15\_bun3 & 3 & 73.7 & 12.5 (12.3--12.7) & 86.1 & 1.4 & 0.99$\times$ \\ +sslp\_5\_25\_50 & 3 & 74.0 & 58.1 (58.0--58.3) & 37.1 & 4.7 & 1.22$\times$ \\ +\hline +\end{tabular} +\caption{Time split by case, averaged over repetitions, with the min--max range over repetitions shown for the Python percentage. Percentages are of the time Scalene attributed to a source line (91.1--99.2\% of wall time here); wall time is the maximum over ranks. The overhead column is profiled wall time divided by unprofiled wall time for the same case; because scalene's instrumentation cost lands mostly on Python, it inflates the Python column.} +\label{tab:python-fraction-summary} +\end{table} + +\begin{table}[ht] +\centering +\begin{tabular}{l r r r} +\hline +Case & PH hub & lagrangian & xhatshuffle \\ +\hline +farmer3 & 14.1 (12.7--15.5) & 18.6 (17.5--20.5) & 15.9 (12.6--18.4) \\ +farmer60 & 72.0 (71.3--72.9) & 72.6 (70.4--73.9) & 69.3 (68.6--70.4) \\ +farmer240 & 75.9 (75.5--76.4) & 77.7 (76.6--78.6) & 74.6 (73.6--76.1) \\ +farmer240\_bun10 & 62.3 (61.1--63.4) & 64.8 (64.1--65.7) & 60.8 (60.2--61.5) \\ +sslp\_15\_45\_10 & 6.8 (6.5--7.3) & 6.3 (6.1--6.5) & 13.0 (12.8--13.2) \\ +sslp\_15\_45\_15 & 7.2 (6.7--7.5) & 6.7 (6.3--7.0) & 14.9 (14.6--15.3) \\ +sslp\_15\_45\_15\_bun3 & 5.1 (5.0--5.3) & 7.1 (6.9--7.2) & 25.3 (25.0--25.6) \\ +sslp\_5\_25\_50 & 48.6 (48.4--48.8) & 48.8 (48.1--49.8) & 78.1 (77.2--79.2) \\ +\hline +\end{tabular} +\caption{Python percentage by cylinder: mean over repetitions with the min--max range in parentheses.} +\label{tab:python-fraction-by-rank} +\end{table} diff --git a/examples/python_fraction/scalene_totals.py b/examples/python_fraction/scalene_totals.py new file mode 100644 index 000000000..68f98a58b --- /dev/null +++ b/examples/python_fraction/scalene_totals.py @@ -0,0 +1,149 @@ +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### +""" +scalene_totals.py + +Extract Python / native / system time totals from a Scalene JSON profile. + +Scalene's JSON gives, for every source line it attributed samples to, the +percentage of the run's wall time spent on that line in Python, in native +(compiled) code, and in the system. Summing those per-line percentages over +every line of every file yields the whole-run totals, which is exactly what +this module does. + +Reading the JSON is preferred over parsing `scalene view --cli` output: + + * the JSON carries full precision, whereas the CLI rounds each line to a + whole percent, + * `scalene view --reduced` omits low-usage lines, so summing its rows + undercounts, + * the CLI emits ANSI colour codes and rearranges its table between Scalene + releases, which is what made the original version of this code fragile. + +The result is self-checking: the sum of the per-line percentages is compared +against the sum of Scalene's own per-file ``percent_cpu_time`` values. The two +agree exactly for most profiles and differ by at most a couple of tenths of a +percentage point for the rest, because a few samples are attributed to a file +without landing on one of the lines the file reports. ``consistency_error`` +therefore allows a small absolute slack; it exists to catch a change in +Scalene's JSON layout, which would show up as a large disagreement, not to +police that last tenth of a point. + +Note that the per-file ``functions`` lists are *not* a usable substitute for the +per-line data: summing them overshoots ``percent_cpu_time`` by as much as 30 +percentage points on these profiles, evidently because nested and wrapped +functions get counted more than once. + +The three buckets together should account for close to 100% of wall time. +Whatever is missing from 100% is samples Scalene could not attribute at all; it +is reported as ``accounted_pct`` so a reader can judge how much is unexplained. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class Totals: + """Whole-run totals for one Scalene profile (one MPI rank).""" + + wall_sec: Optional[float] + python_pct: float # percent of wall time, Python (interpreted) code + native_pct: float # percent of wall time, compiled code (solver, numpy, MPI) + system_pct: float # percent of wall time, system/kernel + argv: Optional[List[str]] + + @property + def accounted_pct(self) -> float: + """Percent of wall time Scalene attributed to some source line.""" + return self.python_pct + self.native_pct + self.system_pct + + @property + def python_fraction(self) -> Optional[float]: + """Python as a percent of attributed time; None if nothing attributed. + + This is the headline number: it divides out the unattributed residual + so that the three buckets sum to 100%. + """ + if self.accounted_pct <= 0.0: + return None + return 100.0 * self.python_pct / self.accounted_pct + + def seconds(self, pct: float) -> Optional[float]: + if self.wall_sec is None: + return None + return self.wall_sec * pct / 100.0 + + @property + def python_sec(self) -> Optional[float]: + return self.seconds(self.python_pct) + + @property + def native_sec(self) -> Optional[float]: + return self.seconds(self.native_pct) + + @property + def system_sec(self) -> Optional[float]: + return self.seconds(self.system_pct) + + +def totals_from_json_obj(j: Dict[str, Any]) -> Totals: + py = nat = sys_ = 0.0 + for finfo in (j.get("files") or {}).values(): + for line in finfo.get("lines") or (): + py += line.get("n_cpu_percent_python", 0.0) or 0.0 + nat += line.get("n_cpu_percent_c", 0.0) or 0.0 + sys_ += line.get("n_sys_percent", 0.0) or 0.0 + + wall = j.get("elapsed_time_sec") + try: + wall = float(wall) if wall is not None else None + except (TypeError, ValueError): + wall = None + + argv = j.get("args") + if isinstance(argv, str): + argv = argv.split() + elif not isinstance(argv, list): + argv = None + else: + argv = [str(x) for x in argv] + + return Totals(wall_sec=wall, python_pct=py, native_pct=nat, + system_pct=sys_, argv=argv) + + +def totals_from_json(path: str) -> Totals: + with open(path, "r", encoding="utf-8") as f: + return totals_from_json_obj(json.load(f)) + + +def consistency_error(path: str, tol_pct_points: float = 1.0) -> Optional[str]: + """Cross-check the per-line sum against Scalene's own per-file totals. + + Returns None when they agree to within ``tol_pct_points`` percentage points, + otherwise a message describing the mismatch. The tolerance is deliberately + loose: a real layout change misses by tens of points, while normal profiles + agree exactly or to within a few tenths (see the module docstring). + """ + with open(path, "r", encoding="utf-8") as f: + j = json.load(f) + t = totals_from_json_obj(j) + per_file = sum( + (finfo.get("percent_cpu_time") or 0.0) + for finfo in (j.get("files") or {}).values() + ) + if abs(per_file - t.accounted_pct) > tol_pct_points: + return (f"{path}: per-line sum {t.accounted_pct:.6f}% disagrees with sum of " + f"per-file percent_cpu_time {per_file:.6f}% by more than " + f"{tol_pct_points} percentage points") + return None diff --git a/examples/python_fraction/scalene_wrapper.bash b/examples/python_fraction/scalene_wrapper.bash new file mode 100755 index 000000000..98bded2d7 --- /dev/null +++ b/examples/python_fraction/scalene_wrapper.bash @@ -0,0 +1,40 @@ +#!/bin/bash +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### + +# Rank-aware scalene launcher, meant to be the program that mpiexec runs. +# Each rank profiles itself and writes $OUTDIR/scalene_rank_.json +# +# Required environment: +# OUTDIR directory to write the per-rank profile into (must already exist) +# Arguments: +# the script and arguments to profile (use absolute paths; cwd is $OUTDIR) +# +# Not normally run by hand; see run_experiments.bash. + +set -euo pipefail + +if [[ -z "${OUTDIR:-}" ]]; then + echo "scalene_wrapper.bash: OUTDIR must be set" >&2 + exit 1 +fi + +# Determine MPI rank from common env vars (OpenMPI / MPICH / Slurm) +RANK="${OMPI_COMM_WORLD_RANK:-${PMI_RANK:-${SLURM_PROCID:-}}}" +if [[ -z "${RANK}" ]]; then + echo "Could not determine MPI rank from environment" \ + "(OMPI_COMM_WORLD_RANK / PMI_RANK / SLURM_PROCID)." >&2 + exit 1 +fi + +cd "${OUTDIR}" + +exec python -m scalene run \ + --outfile "scalene_rank_${RANK}.json" \ + "$@" diff --git a/examples/python_fraction/summarize_reps.py b/examples/python_fraction/summarize_reps.py new file mode 100644 index 000000000..db0928977 --- /dev/null +++ b/examples/python_fraction/summarize_reps.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### +""" +summarize_reps.py + +Aggregate the repeated runs produced by run_experiments.bash into LaTeX tables. + +Scalene estimates the Python/native/system split by sampling, so one run of one +case is not evidence of much. This script reads every repetition of every case +and reports the spread across repetitions, which is what makes it possible to +say whether a difference between cases is real or just sampling noise. + +Expected layout (as written by run_experiments.bash): + + ///rep/scalene_rank_.json + +and, for runs made with PROFILE=0, + + /-unprofiled//rep/wall.txt + +Two tables are produced: + + Summary one row per case: wall time and the job-level Python/native/system + split, averaged over repetitions, with the observed min--max range + of the Python percentage. When unprofiled wall times are available, + a column reports scalene's overhead, since the profiler's own cost + falls mostly on Python and therefore inflates the Python share. + + Per-rank one row per (case, rank): the Python percentage for that cylinder, + averaged over repetitions with its range. This is the table that + shows whether a particular cylinder is an outlier. + +The job-level percentage for one repetition is computed from summed seconds +across ranks, not by averaging per-rank percentages, so that ranks are weighted +by how long they actually ran. + +Usage: + python summarize_reps.py --results results --out scalene_summary.tex +""" + +from __future__ import annotations + +import argparse +import glob +import os +import re +from dataclasses import dataclass +from typing import Dict, List, Optional, Sequence + +from scalene_totals import consistency_error, totals_from_json + + +@dataclass +class RepResult: + """One repetition of one case, aggregated over its ranks.""" + + rep: str + wall_sec: float # max over ranks: the job's wall time + python_pct: float # job-level, percent of attributed time + native_pct: float + system_pct: float + accounted_pct: float # mean over ranks, for reporting + per_rank_python_pct: Dict[int, float] + + +def _rank_of(path: str) -> Optional[int]: + m = re.search(r"rank[_\-]?(\d+)", os.path.basename(path)) + return int(m.group(1)) if m else None + + +def _mean(xs: Sequence[float]) -> float: + return sum(xs) / len(xs) + + +def _latex_escape(s: str) -> str: + replacements = { + "\\": r"\textbackslash{}", + "&": r"\&", + "%": r"\%", + "$": r"\$", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\textasciicircum{}", + } + return "".join(replacements.get(ch, ch) for ch in s) + + +def load_rep(rep_dir: str) -> Optional[RepResult]: + files = sorted(glob.glob(os.path.join(rep_dir, "scalene_rank_*.json"))) + if not files: + return None + + walls: List[float] = [] + accounted: List[float] = [] + sum_py = sum_nat = sum_sys = 0.0 + per_rank: Dict[int, float] = {} + + for f in files: + bad = consistency_error(f) + if bad: + raise SystemExit( + "Scalene JSON failed its internal consistency check, so its " + "layout has probably changed:\n " + bad + ) + t = totals_from_json(f) + if t.wall_sec is None: + continue + walls.append(t.wall_sec) + accounted.append(t.accounted_pct) + sum_py += t.python_sec or 0.0 + sum_nat += t.native_sec or 0.0 + sum_sys += t.system_sec or 0.0 + r = _rank_of(f) + if r is not None and t.python_fraction is not None: + per_rank[r] = t.python_fraction + + if not walls: + return None + + denom = sum_py + sum_nat + sum_sys + if denom <= 0.0: + return None + + return RepResult( + rep=os.path.basename(rep_dir), + wall_sec=max(walls), + python_pct=100.0 * sum_py / denom, + native_pct=100.0 * sum_nat / denom, + system_pct=100.0 * sum_sys / denom, + accounted_pct=_mean(accounted), + per_rank_python_pct=per_rank, + ) + + +def load_case(case_dir: str) -> List[RepResult]: + reps = [] + for rep_dir in sorted(glob.glob(os.path.join(case_dir, "rep*"))): + if not os.path.isdir(rep_dir): + continue + r = load_rep(rep_dir) + if r is not None: + reps.append(r) + return reps + + +def load_unprofiled_walls(case_dir: str) -> List[float]: + """Wall times from PROFILE=0 runs of one case, if any were made.""" + walls = [] + for wf in sorted(glob.glob(os.path.join(case_dir, "rep*", "wall.txt"))): + try: + with open(wf, "r", encoding="utf-8") as f: + walls.append(float(f.read().strip())) + except (OSError, ValueError): + continue + return walls + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--results", default="results", help="Results root directory") + ap.add_argument("--out", default="scalene_summary.tex", help="Output LaTeX filename") + ap.add_argument("--solvers", nargs="*", default=None, + help="Solver subdirectories to report, in order " + "(default: all, alphabetical)") + ap.add_argument("--cases", nargs="*", default=None, + help="Cases in the order to report (default: all, alphabetical)") + ap.add_argument("--rank-labels", default="", + help="Comma-separated cylinder names for ranks 0,1,2,... " + "(e.g. 'PH hub,lagrangian,xhatshuffle')") + args = ap.parse_args() + + if args.solvers: + solvers = args.solvers + else: + solvers = sorted( + d for d in os.listdir(args.results) + if os.path.isdir(os.path.join(args.results, d)) + and not d.endswith("-unprofiled") + ) + + labels = [s.strip() for s in args.rank_labels.split(",") if s.strip()] + + def rank_label(r: int) -> str: + return labels[r] if r < len(labels) else f"rank {r}" + + # loaded holds (solver, case, reps, unprofiled_walls) + loaded = [] + for sv in solvers: + sv_dir = os.path.join(args.results, sv) + if args.cases: + cases = args.cases + else: + cases = sorted( + d for d in os.listdir(sv_dir) + if os.path.isdir(os.path.join(sv_dir, d)) + ) + for c in cases: + case_dir = os.path.join(sv_dir, c) + if not os.path.isdir(case_dir): + continue + reps = load_case(case_dir) + if not reps: + print(f"warning: no usable repetitions for {sv}/{c}") + continue + bare = load_unprofiled_walls( + os.path.join(args.results, f"{sv}-unprofiled", c) + ) + loaded.append((sv, c, reps, bare)) + + if not loaded: + raise SystemExit(f"No usable results under {args.results}") + + multi_solver = len({sv for sv, _, _, _ in loaded}) > 1 + any_bare = any(bare for _, _, _, bare in loaded) + + out: List[str] = [] + out.append("% Auto-generated by summarize_reps.py -- do not edit by hand") + out.append("") + + # ---- Summary table: one row per (solver, case) ---- + acct_all = [r.accounted_pct for _, _, reps, _ in loaded for r in reps] + lead_cols = "l l" if multi_solver else "l" + lead_head = "Solver & Case" if multi_solver else "Case" + + def lead_cells(sv: str, c: str) -> str: + return (f"{_latex_escape(sv)} & {_latex_escape(c)}" if multi_solver + else _latex_escape(c)) + + out.append(r"\begin{table}[ht]") + out.append(r"\centering") + out.append(rf"\begin{{tabular}}{{{lead_cols} r r r r r{' r' if any_bare else ''}}}") + out.append(r"\hline") + out.append(lead_head + r" & Reps & Wall (s) & Python (\%) & Native (\%) & System (\%)" + + (r" & Overhead" if any_bare else "") + r" \\") + out.append(r"\hline") + for sv, c, reps, bare in loaded: + pys = [r.python_pct for r in reps] + walls = [r.wall_sec for r in reps] + row = ( + f"{lead_cells(sv, c)} & {len(reps)} & {_mean(walls):.1f} & " + f"{_mean(pys):.1f} ({min(pys):.1f}--{max(pys):.1f}) & " + f"{_mean([r.native_pct for r in reps]):.1f} & " + f"{_mean([r.system_pct for r in reps]):.1f}" + ) + if any_bare: + row += (rf" & {_mean(walls) / _mean(bare):.2f}$\times$" if bare + else r" & \textemdash") + out.append(row + r" \\") + out.append(r"\hline") + out.append(r"\end{tabular}") + caption = ( + r"\caption{Time split by case, averaged over repetitions, with the " + r"min--max range over repetitions shown for the Python percentage. " + r"Percentages are of the time Scalene attributed to a source line " + rf"({min(acct_all):.1f}--{max(acct_all):.1f}\% of wall time here); " + r"wall time is the maximum over ranks." + ) + if any_bare: + caption += ( + r" The overhead column is profiled wall time divided by unprofiled " + r"wall time for the same case; because scalene's instrumentation cost " + r"lands mostly on Python, it inflates the Python column." + ) + out.append(caption + r"}") + out.append(r"\label{tab:python-fraction-summary}") + out.append(r"\end{table}") + out.append("") + + # ---- Per-rank table: one row per (solver, case) ---- + all_ranks = sorted({r for _, _, reps, _ in loaded + for rep in reps for r in rep.per_rank_python_pct}) + out.append(r"\begin{table}[ht]") + out.append(r"\centering") + out.append(rf"\begin{{tabular}}{{{lead_cols}" + " r" * len(all_ranks) + r"}") + out.append(r"\hline") + out.append(lead_head + " & " + + " & ".join(_latex_escape(rank_label(r)) for r in all_ranks) + r" \\") + out.append(r"\hline") + for sv, c, reps, _bare in loaded: + cells = [] + for r in all_ranks: + vals = [rep.per_rank_python_pct[r] for rep in reps if r in rep.per_rank_python_pct] + cells.append(f"{_mean(vals):.1f} ({min(vals):.1f}--{max(vals):.1f})" if vals + else r"\textemdash") + out.append(f"{lead_cells(sv, c)} & " + " & ".join(cells) + r" \\") + out.append(r"\hline") + out.append(r"\end{tabular}") + out.append( + r"\caption{Python percentage by cylinder: mean over repetitions with the " + r"min--max range in parentheses.}" + ) + out.append(r"\label{tab:python-fraction-by-rank}") + out.append(r"\end{table}") + out.append("") + + with open(args.out, "w", encoding="utf-8") as f: + f.write("\n".join(out)) + + print(f"Wrote LaTeX to: {args.out}") + for sv, c, reps, bare in loaded: + pys = [r.python_pct for r in reps] + walls = [r.wall_sec for r in reps] + extra = f", overhead {_mean(walls) / _mean(bare):.2f}x" if bare else "" + print(f" {sv}/{c}: {len(reps)} reps, wall {_mean(walls):.1f}s, " + f"Python {_mean(pys):.1f}% ({min(pys):.1f}-{max(pys):.1f}){extra}") + + +if __name__ == "__main__": + main() diff --git a/examples/python_fraction/test_sslp.sh b/examples/python_fraction/test_sslp.sh deleted file mode 100755 index b89c26435..000000000 --- a/examples/python_fraction/test_sslp.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# wrap the mpiexec run in python so the rank can be determined -# run with $ SOLVER=gurobi mpiexec -np 3 ./tests.sh -# (do chmod once) - -set -euo pipefail - -SOLVER="${SOLVER:-gurobi}" - -# Determine MPI rank from common env vars (OpenMPI / MPICH / Slurm) -RANK="${OMPI_COMM_WORLD_RANK:-${PMI_RANK:-${SLURM_PROCID:-}}}" -if [[ -z "${RANK}" ]]; then - echo "Could not determine MPI rank from environment (OMPI_COMM_WORLD_RANK / PMI_RANK / SLURM_PROCID)." >&2 - exit 1 -fi - -echo "^^^ sslp_15_45_10 ^^^ rank=${RANK}" - -python -m scalene run \ - --outfile "scalene_rank_${RANK}.txt" \ - ../../mpisppy/generic_cylinders.py \ - --module-name ../sslp/sslp --sslp-data-path ../sslp/data --instance-name sslp_15_45_10 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 2 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 - diff --git a/examples/python_fraction/tests.sh b/examples/python_fraction/tests.sh deleted file mode 100755 index 6eff173af..000000000 --- a/examples/python_fraction/tests.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -# wrap the mpiexec run in python so the rank can be determined -# run with $ SOLVER=gurobi mpiexec -np 3 ./tests.sh -# (do chmod once) - -set -euo pipefail - -SOLVER="${SOLVER:-gurobi}" - -# Determine MPI rank from common env vars (OpenMPI / MPICH / Slurm) -RANK="${OMPI_COMM_WORLD_RANK:-${PMI_RANK:-${SLURM_PROCID:-}}}" -if [[ -z "${RANK}" ]]; then - echo "Could not determine MPI rank from environment (OMPI_COMM_WORLD_RANK / PMI_RANK / SLURM_PROCID)." >&2 - exit 1 -fi - -echo "^^^ farmer ^^^ rank=${RANK}" - -python -m scalene run \ - --outfile "scalene_rank_${RANK}.txt" \ - ../../mpisppy/generic_cylinders.py \ - --module-name ../farmer/farmer \ - --num-scens 3 \ - --solver-name "${SOLVER}" \ - --max-iterations 100 \ - --max-solver-threads 4 \ - --default-rho 1 \ - --lagrangian \ - --xhatshuffle \ - --rel-gap 0.0001 - -exit - -============================================== -#!/bin/bash -set -e - -SOLVER="gurobi" - -echo "^^^ farmer ^^^" -mpiexec -np 3 python -m scalene run --outfile scalene_rank_%r.txt ../../mpisppy/generic_cylinders.py --module-name ../farmer/farmer --num-scens 3 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 - -exit - -echo "^^^ sslp bounds ^^^" -cd sslp -mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name sslp --sslp-data-path ./data --instance-name sslp_15_45_10 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 -cd .. - -scalene is designed to attribute time to Python vs native (it can estimate time spent in compiled code called from Python). It’s often the most straightforward way to get exactly what you asked. - -Run: - -python -m pip install scalene -python -m scalene your_script.py [args...] - - -It reports per-file and per-line: - -Python time - -Native time (extensions, libraries like NumPy, solver bindings, etc.) - -MPI note: if you run under mpiexec, you’ll get output per rank (may need to direct to separate files): - -mpiexec -np 4 python -m scalene --outfile scalene_rank_%r.txt your_script.py ... - - -If %r isn’t supported in your shell, just set unique outfile names using env vars per rank. - -This is usually the quickest route to “fraction spent in Python.” From 5faa57046a2508fbd7593e52d5ec03707190941c Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Sun, 9 Aug 2026 15:58:46 -0700 Subject: [PATCH 09/11] python_fraction: version the built PDF The writeup is the point of this directory, so ship the PDF rather than requiring a LaTeX toolchain to read it. Co-Authored-By: Claude Opus 5 --- examples/python_fraction/.gitignore | 3 --- examples/python_fraction/python_fraction.pdf | Bin 0 -> 131303 bytes 2 files changed, 3 deletions(-) create mode 100644 examples/python_fraction/python_fraction.pdf diff --git a/examples/python_fraction/.gitignore b/examples/python_fraction/.gitignore index c457b6f1c..71bca27c9 100644 --- a/examples/python_fraction/.gitignore +++ b/examples/python_fraction/.gitignore @@ -7,6 +7,3 @@ results/ *.aux *.out __pycache__/ - -# Built from python_fraction.tex; not versioned. -*.pdf diff --git a/examples/python_fraction/python_fraction.pdf b/examples/python_fraction/python_fraction.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f19bc7dd6eb25c7ff7c024393bf77f673ad70612 GIT binary patch literal 131303 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 z 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 literal 0 HcmV?d00001 From 4499db9a30bacfa35dd5b80815b3df08d26cfdfb Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Tue, 18 Aug 2026 14:32:43 -0700 Subject: [PATCH 10/11] python_fraction: address the Copilot review Portability and accuracy fixes from the review of #825; no change to any result or to the generated tables. - Invoke python3 rather than python in the three bash scripts, and use sys.executable for the `scalene view` subprocess in make_scalene_latex_table.py, so it is the interpreter that actually has scalene installed rather than whatever `python` resolves to. - Drop the `bash -lc` wrappers around lscpu, sysctl and vm_stat. _run_cmd already sends stderr to DEVNULL and swallows a missing executable, so the shell was only there for a redirect it does not need. - summarize_reps.py described the per-rank table as one row per (case, rank); it is one row per case with a column per rank. Co-Authored-By: Claude Opus 5 --- .../python_fraction/make_scalene_latex_table.py | 17 +++++++++-------- examples/python_fraction/make_tables.bash | 4 ++-- examples/python_fraction/run_experiments.bash | 2 +- examples/python_fraction/scalene_wrapper.bash | 2 +- examples/python_fraction/summarize_reps.py | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/python_fraction/make_scalene_latex_table.py b/examples/python_fraction/make_scalene_latex_table.py index d76ac8322..1cc80510f 100644 --- a/examples/python_fraction/make_scalene_latex_table.py +++ b/examples/python_fraction/make_scalene_latex_table.py @@ -38,7 +38,7 @@ (with the colour codes stripped) if you want to cross-check the two. Usage: - python make_scalene_latex_table.py --glob "scalene_rank_*.json" --out scalene_summary.tex + python3 make_scalene_latex_table.py --glob "scalene_rank_*.json" --out scalene_summary.tex Options: --from-cli Parse `scalene view --cli` instead of reading the JSON directly @@ -57,6 +57,7 @@ import platform import re import subprocess +import sys from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple @@ -202,7 +203,7 @@ def take_value(i: int) -> Optional[str]: # --------------------------- -# System info collection (Unix/bash assumptions) +# System info collection (Unix assumptions) # --------------------------- def _run_cmd(cmd: List[str]) -> Optional[str]: @@ -272,7 +273,7 @@ def _collect_system_info() -> Dict[str, str]: info["cpu_logical"] = str(logical) if logical is not None else "unknown" # lscpu (Linux) - lscpu = _run_cmd(["bash", "-lc", "lscpu"]) + lscpu = _run_cmd(["lscpu"]) cpu_model = None cpu_mhz = None cpu_max_mhz = None @@ -301,7 +302,7 @@ def _collect_system_info() -> Dict[str, str]: # sysctl (macOS / BSD) if cpu_model is None: - cpu_model = _run_cmd(["bash", "-lc", "sysctl -n machdep.cpu.brand_string 2>/dev/null"]) or None + cpu_model = _run_cmd(["sysctl", "-n", "machdep.cpu.brand_string"]) or None # Physical cores (best-effort) physical_cores = None @@ -312,7 +313,7 @@ def _collect_system_info() -> Dict[str, str]: physical_cores = None if physical_cores is None: # macOS - pc = _run_cmd(["bash", "-lc", "sysctl -n hw.physicalcpu 2>/dev/null"]) + pc = _run_cmd(["sysctl", "-n", "hw.physicalcpu"]) if pc and pc.isdigit(): physical_cores = int(pc) @@ -345,11 +346,11 @@ def _collect_system_info() -> Dict[str, str]: info["mem_available"] = _format_bytes(mem_avail_bytes) else: # macOS total - mt = _run_cmd(["bash", "-lc", "sysctl -n hw.memsize 2>/dev/null"]) + mt = _run_cmd(["sysctl", "-n", "hw.memsize"]) if mt and mt.isdigit(): info["mem_total"] = _format_bytes(int(mt)) # macOS available is trickier; best-effort via vm_stat - vm = _run_cmd(["bash", "-lc", "vm_stat 2>/dev/null"]) + vm = _run_cmd(["vm_stat"]) if vm: # Parse page size and free/inactive/speculative, etc. page_size = 4096 @@ -377,7 +378,7 @@ def _collect_system_info() -> Dict[str, str]: # --------------------------- def _run_scalene_view_cli(json_path: str, reduced: bool, columns: int) -> str: - cmd = ["python", "-m", "scalene", "view", "--cli"] + cmd = [sys.executable, "-m", "scalene", "view", "--cli"] if reduced: cmd.append("--reduced") cmd.append(json_path) diff --git a/examples/python_fraction/make_tables.bash b/examples/python_fraction/make_tables.bash index 17d2fb253..7f9ff1e30 100755 --- a/examples/python_fraction/make_tables.bash +++ b/examples/python_fraction/make_tables.bash @@ -28,7 +28,7 @@ CASES=(farmer3 farmer60 farmer240 farmer240_bun10 sslp_15_45_10 sslp_15_45_15 sslp_15_45_15_bun3 sslp_5_25_50) # Primary results: the persistent interface. -python "${HERE}/summarize_reps.py" \ +python3 "${HERE}/summarize_reps.py" \ --results "${RESULTS}" \ --solvers gurobi_persistent \ --cases "${CASES[@]}" \ @@ -37,7 +37,7 @@ python "${HERE}/summarize_reps.py" \ # Secondary: the file-based interface, for the contrast noted in the writeup. if [[ -d "${RESULTS}/gurobi" ]]; then - python "${HERE}/summarize_reps.py" \ + python3 "${HERE}/summarize_reps.py" \ --results "${RESULTS}" \ --solvers gurobi \ --cases "${CASES[@]}" \ diff --git a/examples/python_fraction/run_experiments.bash b/examples/python_fraction/run_experiments.bash index c17de5e54..4c06f2959 100755 --- a/examples/python_fraction/run_experiments.bash +++ b/examples/python_fraction/run_experiments.bash @@ -144,7 +144,7 @@ for c in "${CASES[@]}"; do # Same case, no profiler: record wall time only. /usr/bin/time -f "%e" -o "${outdir}/wall.txt" \ mpiexec --oversubscribe -np "${NP}" \ - python -m mpi4py "${DRIVER}" "${cargs[@]}" "${COMMON[@]}" \ + python3 -m mpi4py "${DRIVER}" "${cargs[@]}" "${COMMON[@]}" \ > "${outdir}/run.log" 2>&1 || true if grep -q 'Cylinder finalization complete' "${outdir}/run.log"; then n_found="${NP}" diff --git a/examples/python_fraction/scalene_wrapper.bash b/examples/python_fraction/scalene_wrapper.bash index 98bded2d7..d0b521a54 100755 --- a/examples/python_fraction/scalene_wrapper.bash +++ b/examples/python_fraction/scalene_wrapper.bash @@ -35,6 +35,6 @@ fi cd "${OUTDIR}" -exec python -m scalene run \ +exec python3 -m scalene run \ --outfile "scalene_rank_${RANK}.json" \ "$@" diff --git a/examples/python_fraction/summarize_reps.py b/examples/python_fraction/summarize_reps.py index db0928977..34e1b8710 100644 --- a/examples/python_fraction/summarize_reps.py +++ b/examples/python_fraction/summarize_reps.py @@ -33,16 +33,16 @@ a column reports scalene's overhead, since the profiler's own cost falls mostly on Python and therefore inflates the Python share. - Per-rank one row per (case, rank): the Python percentage for that cylinder, - averaged over repetitions with its range. This is the table that - shows whether a particular cylinder is an outlier. + Per-rank one row per case and one column per rank: the Python percentage for + that cylinder, averaged over repetitions with its range. This is the + table that shows whether a particular cylinder is an outlier. The job-level percentage for one repetition is computed from summed seconds across ranks, not by averaging per-rank percentages, so that ranks are weighted by how long they actually ran. Usage: - python summarize_reps.py --results results --out scalene_summary.tex + python3 summarize_reps.py --results results --out scalene_summary.tex """ from __future__ import annotations From 5beb4284f1d4bc519a0c156d3e2a09477e60e0b9 Mon Sep 17 00:00:00 2001 From: Dave Woodruff Date: Tue, 18 Aug 2026 14:39:16 -0700 Subject: [PATCH 11/11] python_fraction: sharpen the answer, rebuild the PDF Lead with the practical answer -- for hard problems, not much time is in Python -- before the "it depends" that follows, and note that the fraction can fall below the 9% seen in sslp_15_45 when subproblems are harder still. Drop the paragraph deriving the fixed per-solve Python cost; the practical consequences that follow already carry it. Co-Authored-By: Claude Opus 5 --- examples/python_fraction/python_fraction.pdf | Bin 131303 -> 131541 bytes examples/python_fraction/python_fraction.tex | 18 ++++++------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/examples/python_fraction/python_fraction.pdf b/examples/python_fraction/python_fraction.pdf index f19bc7dd6eb25c7ff7c024393bf77f673ad70612..4cb369d0ecd5167bca22cca08b4124c6c2e0af61 100644 GIT binary patch delta 30142 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 gwT0$crwUT6Rr9zat$arn=lqQk%*%Bf%>#u3uo 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 delta 29857 zcmV)DK*7J&g9zt=2nZ!mL`E$!E;o?~90M^nIhR4$0Vsd18EcQ@w(EV#l?QWVM-+57zecl8G`o$uL!{Kl^Z%3QMCfa;= z6TR?#eRuPhZ&S5VMHs6r-P}EFRGNe;Pd0g(gju9Ecl*t|;M;9c2DWZSGxXbC62-yr zfL|5dnNEK*aJfRT-FpT{E*`T6b#I62-Z6;@dm)5$6i zRk_*4Wmr~n@L%g~Tm&D@9ySBHzw_s(ZK;9*4k+YzZ>RT>N|5SimlR=|Q1$%lm_F#-(fszD!h8i9c%+iPO}VmDh1)C@Ws1`41n`SgVgc3IIjXY1qWsDFl#S@@s((q zv=`UMV?8pgJDHt3o$&Ra8A7^TqAtTzIEH_{S4nDw8mshYOsE!w;l#TJr}uKiz$+*b z(=rrigtiwx9<3QiP4)p6%6Ai$qvMLoi!LsspsoAEgfin5H?d;#j5qcs#FYxj4yH~7QYHbxBcTUd!0?%HZG}kPwhVvq zEb}uE!*DyxK+q?B7_87FEzP5qmR?*R^&u-k;mlyo6{UW+OOhlQdbDvE=qGu#W<-@u zQh8!rBX)4GXa&vyPX#A(3f}r@qmgxST{M0)ct>2l=M6k@9o#*n*E>^Bxx-I*S47RT z;K=q)!bV6QOsrliK;1wT<`Y&%RuX@5RRC|`5Wx9VWbX*uHQP+VBWRGt6*=JpCI#B4 zY4G$T6D7w3yZ73eL(eSm$+!ydX3hOHdWP8TuMlS80PLfX>cL|y7tDh9Q5Ko;iuHg|Re&bnvAZt4l_q7mSfYO&ST(X|F)PGenH4Rv#Ay6rhmMh$XAnmkC1-!)K(eC{ zf}M^*X!FaJK7-pRFrZwi;Ctca5myHL!SUXE4h&ap+0`9*)c$ZqRm79s;oD~sSmY6G z6^GzkdiTi2$xHZT2xrYk2)YK%KwhkZe&D%-<65C9HjTgcEK&RAc2`s>s~n;%rh%LH zdiH^GX5DtEs)G~WL&965KmZJ@1T~QupWw}Wc6)Kf(n(pS^&(UTNi=clnVN#WwM?&p3 zkdq}PjM~lZ&A&KpJ3CKBRanFYkcz@e$*C&4aweu8fbjn~6OxZAOU}8E8&3bA5l=J5pwAVm^^S6BnFz0~6NV*@U z!_n_Vi$|!?$S;f|53C+1+dIYrYb8s?1=DqUZ@{~@KAfj%DXN{L7_a!NhuU`9V&F(i z2}@uO9f$~$wqHO#qvT5nrW`oU7Cd-B3Tr&0Km*@ToH?%b{GETJ35|*uhaSCQmg>7MF}96#XFi1U%o^ z%05bqC~E*8;S&hQYx9O$vIU6(0<@!f0H^yWF#$JPxQx$4%td;W2D+XD86Gn0mN4t} zgqIr-o=_03EUJGQl@fprWmWp5{F%89mf?-)iSxjs?DyoMS=~ zMUWlYJ%jm~0d#he2%W)~Y@ss-EF+00R=2uk=y1;vMi#!1z5Y3=paf+SzRblDV3S0Y zU9=+Qzi)*q6@xDdVOGiF!oZT?esa&OM-PKtSq>#@tthYvna2B}Gkv|NXyOY|vX8Br z)AW_Eys&@hmy`F|HAu}{x88z!r=d*6(B>^!e@UU&_gNsmtRqJhMo|pKI}le={lAY@ zbg|_k*kqt#-=nEym{a1bOqOS7(E|Y=NSK(i;f%$McBGwIzrSj4Na9H+CNk?;nqkK%*Ul`6&Qa@2_`-e=Z; zn2cHuX8#(e3kMW;UeXOZP>JJ)?_u%l%KKv&oasBv!41mFDoClh5Yy-yN?nKiR&`|K2Z$QuKs=@wB z@*9!+Dr4y#ek0tHndcxwO1ofWC+ZkxUb$cZS&#N0t7$07+A|ulfe345&+q)H`SXAF zZ~Q%y|G6i(rTk)BA1v6vp=ZKVr2XbipXiVfX~3sGfyTNcJ{|U6K<5gdr)Jp&g;&>J ziMmZOxKQbVC7?6?2Oce17|wp@j~nCS%>gD|$rbPpKLt_JO))s_!MNs=0(u!z%zAPy@-f2J@{6pV19BuV(vrZb+1w@@bCuL)#cGY<7hRAp5S z;yOR9KCKk=n%90I>0Kwf>)rzu4@>Dm`2y5i%~*r`eL1&(t($tH9T19L!`**7n8aKq zXbL`bk1EiTEj=@WSX(Ym0U#h;26zht7*^4ap=<}aPy7%Mne&^a*F$Y5CL$!Dh7!6L zMG-*!F1>p>(;toh2cdvy0g;eLOdkfrz0;Pd?im7Z?LS@p#1ph{%m*?M!JXgZPLdWQ zAuf1{6m}H|Ht=`Jq;V5h)i{6T%o3?j^WF#k)-w;}a7NFiz1q>Eb7Jaoaxr|u6p`wv zg7a2bebA3Q7^UnzI-c{HGv3KJl_Ac6Wsp9}4oYx54!ja`H)1!E;rzNVIPa?=q^-fQ zt*|o4K*S#hd)*q}4&?R#fw=+sX^-n2lcCN)ay>+xH+LQVOH@*g2c>@$`uc^rf|VD( zk4K*{7KL{hFcK{<${CUE_cTHonEA*EU?+v7W9-M7f?Tv%+Mk7Yl2(x0!TWqdY|R`# zh%s?ho|@Hb&P#G)lr0yf@77$=6gE!$8-n}ViTo>ug%Fv#_ZowDD%_~~lgcE{qHBF! z{1Ygx&?;1Q1?wcD;k4Ki89LcJnY?Re~@RCkrFjcxO_dq~%ok3LFO7V&&ZOjGqX* zD|qvfffmA`KX2V(ssffDkr_OXeEM-{JzE!&eRWpMY+_Z0DJP;?qI@E{M)~IM=D!(% zi>3-?Ze(+Ga%GcP4IcwGG&Pq&*a0Yi#al~r zHeTIqA0QGEk;Q`nz?HP|kI&cB1B!q^X;p$N7jd55Gu=J?LMvL`tfJKqZzBKu?d6*< zzgKdVhG{J0)#ddnF=3?Am6aw`TCXm*t6zhn?=F77{KuEy$Kqt=uWO}}V7+dC^X*}i zZ+XQC7O%K&+Uml!nu;KynFeKZA(P;Cedw~AoM+3T&O2^)4gdDLoR@cp%1<5YZQgF0 zLrwepFL*kuw>(?){@0_~G-Y#WFGNs$0wfV^{f66Ju^|9EkFO|m23X$HJwTha5MgQz z6X2?CKF~B^Xeszu)M)t@1bJP5Wj%u|>KmRd>dEpvYs(^Ux%1HhY`dcI%bM$9Ih~$b z8;}8uwt0B5wowwi-()za2bT4HTU;G*x87aN?@7gf)Aa2a;w_=d%F@Fxs(kK}J+NsN zgZNcGv<~gYx61;U>ilc;66>#o;k`Q{IO6FCZgxC##WUqVA{xuu0rC!iO}Eel4W0g) z+x;%<{ZhVO55aPMIyIQ5W8tL34Go8YFo@F)GnZ@gDl15Ih~di1hYq&I$6}jrd9XS3 z+-Ng^_A7Csn%B~WY>+$lQhy{gZtVt7q39W$cJy>$>)bc4{lv}>TpgUmuxRyN=ngJ) znBw7$PX7(@PNs3t9_o&NT82qULnJ=*oAG#u^Ob(8$_9z_1Gh!LPzF|qR#Bp_Z&iyA~VSwHmO-S@l3i|BQmZF?#&fsFHNGsownh zd$ihO5~L4RnyhZ!(rP8c6bqtNxqAQRUo%!v8eq1u10_;_zFMq-Sv*CMwV-XOKqC$9 zu{gZScWFlFf3^s!Nibd!EP}WF3+J1N66|k){n5_sCs}{j*;aLeW z#>{VhmIKLu7iWAfHBTg|;&73JN(B;O#ps}Ab2Cur@li#5{;d73&9iOy_`FKtwWdoP zSm`K~GM(@grsOeB6Ng6IMUa{_j1%*io%1A;Prnxcv&%mhBSMLTEb&|bDpJa;jqGj) zsWzdNViBY|36pq&bN6YUutq?R(kH1);H+Wt%9l`ow}O2O6hd2#)X8bm6>R z4+ddvqzRVQmG^o?yrr-#Fg#2)HIby@&ZeO|-1DaPp;-U5?>-+-t zq|xx$!PMstARiropo2&sItF#besuNG_OTBK-qr=)d{&NPrw6<*UGy6wC`xs3IY6X? zcXRRb=lcF2IBBpK=$GrPt@2jQr;E}g_yZ?H5T*F1!~L?hXehSQLI)y>cmi=Y!gP** zb=I+tg75OO@Hq!emS2&ga=8Gpl5xi@lcC-#^@vhV`gna z5&Xg$SPg=F`T0*PVo|rBw;4!xd9b|2+J;UOc^Qo z1?G8fkPDRxwFpj;gb@P6XW4;&&@~i#5tQ(kq)q0o`A8*5D@f1N5BTMNIkiaxhJR1h zJn_l+g=3vAb@21w-_cAoe%7?q=YrAVEVs+h?tKd@Vj zPxU9M{HHa7v=;8ESVAKZD52%^8es{XtPw^ipVA0Rp!6Ey`D?82CkS#&H-6 zJ9p1nmiP}bv2iKGoSP>e3Uwcx4(c794(L4RbkLyj8%K%=1A~9Tn#bU5cH&kJPKPl6 z9~FaVAHOomKd2ahI)0IVnbk1taV~*Ut1y=77^R$5F&gwr7QqP@JTdX)n&-k9#o>&0 z){BF()8K{p9r+B*$gF8S3RUz&Y@*oSIarfW1kSE?9}Gr;u?x2~eNJo<8ghLAdhRoS zDvp6VjXeP5{`3PY3?O;gG7ajc=OtHUCH;1i@~qx?bY1a@N(WMZM6SmpoW{Jf!9;pa zP!YU^|&3O%|tq2uq-34&M+Gv0n;nefmh)BSR5yzgc=$W-D zNkffA<3x$ew&)-U?OSS@AqAv^G>qSJixW3rqXSKp`?TLZ&keaFXuw^=O+5*ZFyKD- zyBTtzyd+YWD{9bxLImiS`YZ=w@tF!YPOcbg!95pG>9ljr%abPVeiFM<|f%;&0UJ)2IwB9e$*R=<#I zI)Ryr7Lj9U%P>_FIiAzzCX(=>?GiXmWZ*+jXgKH38WF>XUPL+#96puGxqD9dw$|{& zlu;8y;i7AQO-9_6dr>D1I-kdPS!bGc`n|a04i&xmaTga;W2o`ro-0t&i7U}`0DD&m zML_)e!mnf-L{(9dgsE|1IfkpOhuh6Ss4x^>d zY@|TVX^3vJyvjMfnbk#C@lbxZFN;mlyA($p{JrKa-nhD01*_Wp#`B1hq+RDL0)0xG z2A!#<1}J#8_>Hq3T5@qFT7pXyF>+c6N#m}p5aV1{hykjbJ(KN+58LgVeIM@czf#Jc zNy}k>of=id0y<+C4+nea#P;}w_7NuO5;pm~6AnNd(1A!9r@{B+#hx)*wDg?H0^On% z-oFfudY#Hjk}%TNfr8vU;ee(oK$#_QlJ=7b3`RKTRh9}BBF-gnLi?GyTRv7;RKh2w zGU0a^%u}fGC;NJ}pX#e)1F9RkXi*|+Ylk{@TFgJ!Al;$$#m49(zVKUJG9n`Fc;PfTwcA5|dJ;yD>EQ%kchG`)e}JZc zj`~2x_I#RtN`*ni#)Hm`-A5qe+FgNYghv`EN5nDz&NmMXr>w9uE0~9Rb;Z^WOfvy@ ztcT!oUItk6?}^cgPd@#IT;hAl#%wr1JUIZyZ|R_r8jp#-%K9U@tkYaEypszXdkaV{ z#}aEFrmCSlzvTY0Y~02}*|;5Qu6fLVZ+uiWO<)=P$*LnkNO%%E($JW8LV(}=4#WY; zxC)&R?v7An{)dQ@)m&ivvFHk47%{C$rf7mw8>EHVEZ4r&c-vG(&Boi?b-#LeXy+@~ zx#GZzV+wCQlqB$Q?+P_sb0~W5{LHZP-H-h1u6MZN4xi1+hlhf8nccKmeN!-h3j{k^ z9{$6HakFaS8%S9=j4wwh5$+=uU@n z>!-WIWoRZV9>kxzd}|uds-ZQ?3CN*zoxnJOQh1_A-eCIiW8l12WN%_>3N$#85fcJAG?UR9Dt{Zxa@@G_uCJJjYpE)WA>Lqe zinp?x-AX0ST2(oC_dw538pa$_Avu;+vmc*sG(dtQT9#M5Xz*xsH_(lK5z02Vn{4yr zMRxZ4zY7Mx*@kWMER~XNj8G}d>}Gqoc>i0r*}>e;n=BQ%-P}9K!$u00GG?J&Z{A(} z`{K>@#ebiE5PXxTx#oIveY44RDp|fUT&GHIu6LXF$@fKHUS1iYZSqTb*I!;So@L2@ zF0Dw4y1u*;LL|RtD(m47yS(Cg&Xczf!@g~%xmPsjR{paXs!x~PCgt+{yN7-#4>%_u z&HalOcKfo}UH*3c^Tpq;7Y>5+)ByLZ90ZdovwxaS;-=^hWhdY;bS@J36%x2nsqow} zo$_f#7Gm3lE_TmolWN1R!n=7BSv zvVVv(mGYA@X%=(FrX%CXq~}cXRXBU3L`-I>;u~!&{yd>WunC(#p+O7;pt9Uy5}Xm? z566bVaR{yTcpN+%k3;iRPsgEwE35&I5FeY2#4A{)sA63x71h_!w?hp~>^qFg>R!)w0yNS%)Ubodb&b zK0)Y8C^HJ(NO%1W2;HV;k~wJIDW6>co8#qm66d4IU?9J5lekW;)L!KJSt3_5)oc}s z118CyA@G+|4DyaBJ}K^(Qw-9c?{N|~x(Jp@cUMu`d3?ija1nqjmNqDWmo`JQ;D4pf z*dLen^Dejq)1%h8;DAa7s!z&$1)4$NG0i6>z5>l4@~`ewiPUnv3QYwPWoIfC<=Pw1b;4Bf&j^=awf6P2YZzZt{;5Tw;33X8~Qet6IrVBd=-@g zRG#LdzOmH6by$NSmnj6=F+sjW5r1NFo~u;x=;QBNWcRRdx*~N#tMD~EYKO^s1a$OzV zh)Vjqx*DkY0aJnpYHuM|d5wo6lLCi*x=`GfsPj9**7_L(Gc?kbci2eEp??~xw&^{J z`)b$|-eK?Q9;yaz(CL8aiqC#^(cJp!HX=$R=cs8(zju)L4Ib^I;IwsXMW8yRQ#k0+ zb=$H*6a(xn#nZ9L+spHbo|Y6{Nsab~@Oi{Q!5^qd-ffGzY&^YU7`p1iaVU2lto4X! z8gsy;J;Ff%F`B8X#g(6|Of^|6QJQj#avVO=aVmsq;dZCf7?4YdQQyRQ8e z^12#!9ux|(-XjUmp&||!#Ja0KRXca0xt)|Y&y!;l@P^rG%h>Zm0)I%-3>SQ#Ml`%6WFk>QEjihX-<`AHaV|n0IBR2Mk8U6L5gEgt#NN}s*?q3uw015#R z60*iTcvD|=;F(ZZVGz@9E3MwTI8=;26v6 znPgDCL+t00Pd&RmDB1Z<7x7(%n{H1ml%Ghj&(AA^WVMFS(ySs`K$UU(49TuQGe|b3 z`J`l5pcy24woj6*iEGQ?{Xnv6CRr0tgOLqIB$BL*4Syf>GFKpcy0^(|l60E6@y*{n|c3RH?~Up$XO=QcpRj=_>}PFlv@3VwyO!6ef-!bee-c z@kpPziY*Dp_3%ee`sB(1>YMaQf@@;ySPDReN#_>}0vI^Jk~K&|M5CFSlUytbSO73; z6`GJTGk-15wwd{IjWb(-oEJwEz;%FgW z7~T460A%|q4|)?<1EftjRs*sqD~7R!te7UoL`y=7^Z;q{^+LwLV&+{CN5(P^w50_-`(siK? z1dMgv1GD(;3(WH?8ZB-M%-<*@MUU;rF*$S<9L?eij+E ziET!PgU><>hp6OJ1`c&eh*d~hfH!PLa>%!c=W9}A2A5Lxk#)lZ>xLzLTYn-_#+NOz(H}qDbuAD% z5ERxYalE}7D%fc?kkJRP@!BQ+fQA65Jh;V;?P)0%BXOo0dTO}ZTibo~!##loUw?yR zw3XH$5}i_c=ecCb?@POjPT2tp6AG8m=)r3hUDZ;r?Z8E$`LH^{s^I5w!4|AZwJQd{ ztZAEq&a>K9ck!zlnEZ69!1~n3viGRS5+N3Z+L!L<<6~yJvOm@XwST7{=kgW`8r${& zu5Bb-gPRo`bfg*vls~q8xfVyun}2~?VEw9HnBV&b3YsdlSk0Cq`6)p6^jmni0~K1K zdD9hJDqUjFRh1EAI}{!uh|*Ohv!n-Q^wg>|m zPVM2{g(O}pkLv(;62V-_zNEN^kfq+`$PMk5`^Pfeq}GOL7GpuixJup}n_XS@UwB^w z(O*t092{*|n&j5&M^9baFf>Ty6mMbw&~|>t71Z4w#Gbcu9%>8nM*%42ZZ!_M^T*$F z3QaK_JEslM0mY_xuEkF{?teTy*x>OL^s}#SQF2mGHEI|Q+LkV<$4_2F`?9|4&(&iT zU{}@^gt5UeIVrnaIqHy1g2-k%B%RHHGZ~Lx*(dcE9jq0g8$;3^@_89s@@i9o;A#rv)n}5#Jt${d~zN298 zrfdCLtf0Q4b-Nbz;J)!#TJnxd+rf8J7)IG}qR`kL(WGN5`hqlm^TJlh4FrAK!k-%u zvb#V2YZ-okx@nKj9_N8goTN8wIPJ6tM(7zAw)%40Haqv7DHn9qKD>OnhG@9koe@P-$rol*MW9!@xRd}{q_|;wYsef{20;d1*_7aY1;pXW1 z%~=Z7xl~^&azJjZ1SBIYORZEdi71(b5M1XDm6lJvqf__})~-TRsnq6rPIIwn26nxHyNFJjuAt9tnn;z6;zKV{(Icr zxaCi8;gpjyXCRpPaDpFFUO;MFo`&_%{6}?A6AqEVhMz+J8 zFi9yNgPq4Q%Xs4{pmgHWc%dmX!!9S@;CJg(f`75u?V_o&B140-XNJbzTATcpCVfE5 zqW{Z)(I|al_RwFiX6fWqot@aHIvvC=fbY4mytKB{AG5G2a)Lf&u#ii@(DY$&B^c%)9vU zjDL+E&zRZcnaTq=_OWr}bH&VO-wtT#&!5`dU?x}B^QSfrCSaZ(*FYRhudl{;K;enc z=#}!tYIwht!22B2fTx*UBKAW{;T-@RH@%jEsfE{4!cMQH0HX50Q`w&C-54Kqw?0t# z<;M#&E&#^nyZ?NYd7m5rmz$HB=%%xLjoW>_`Jq@Ab#v%W$AP-OZU}_SR@Q3E!HmZo|=q;`$ zG59HrEgc$Xj%d)CSNG!4GlEZdPv81v{5=IHlN>m{@iB0T5XS3^n1Mjx_kR&};Q>UG zJuD{!Ha9kxLD&H(f2~+akJ~mBzR$1FWe^#NREgSqOM$j%(L;(%Pe~56P1{7ZxTG`- znjhb{$zvs&Lync0nI|8^7Qe`y{*oNQ~DJDzA!hwq=p z)6hpJqfx;96H^b`b;j}0o%Y5v*-bq!^vJw4&egwZTojwncfW(C^_8X{!%s<~grOFU zcoUUb6Mh(2nkWNWxoM#mUIsrzX&N_0!Nwk(-i>zXQKcjg4+E;1WFiD9v+h8gbqBI` z!uLaG9!_Kdf1GHCjjF;2G~Qe!>7I{@Od#oPE7zSlZz4E7xY2_hd6tKF^V8;WXU?b` zZ>?wR%k@S}F)bY{+O@FK{{ zS#P^D;TLGFc1ID~o5xjmjEJ8sBefw6BL17~m z@WF`y07PfBrx?PTeS^~FF~BIPoe@xEaA@pe_8OfsdLB>v^UM;29kVXG=WWltfM~jVoP{5v-wqMpc)WooX=jlh6&CbZHJ)1 z^gx1@F91`n3=DJLNi;5u=Vy7Ng~SOK7qxJ+YX?u*eq21uwW1q;@wD}JGx@#2eI zn?x{U92U0U+k3m8v^>rAz_^_hVSS{U0~~)Ergp!gmm|}0rM+l$K081u`RCDgdNeB# zC2wQw8 zsVI)XK0ad=^kmxJP<-GJMHVP?(XUx6__kW2(zZsfy@w!NJq(^T%RASm!q#c=K58kA zqYk^Xjr{q1mCn#8%J3aHZl#WC0x=W=f70}lFVFP}IK5$3r@4ID-n{*a<%d=>>+M{B z_H&_fMh0#)rI);&l-g<|wQc7`YfiIUh1;%d4f3B+n z!v`ju0M^?dw}J&D>Kjbx*wI?83xZ$zbdiIZf3)udb^^k%aq!lg{&2fAgUuw1JVA zQmtS`H2}Ua^Ef>VlY*2t(!|~0EJ$+MG4G-xx~<3-vktJKsf?Ioyy4-CB+6_^C857q zA~L6Kv}Q33dN2}q{yr|N+E~}-D$@)(T2#GXDBcVDt%tBlh zb)4SfvgY9yNL3N%w{uGu&|Mn-;&PSqcfOe_A@Jnw5rQhEn_N7yGOoe?1&9n6YD~CF ze!=15f@2p+QJ+}Mx0MP{uVP9iP`NQKSrOMs{(TV!)su`wm5HdHf3C(Dx?+>Q%9{}5 zL^aDd^R+iK6{py#Ug1p;p#V@zJCm1wSE{;z3Nv^K8Wkp*WGI$20pe;4wE=(A;kvu( z&R#@Z!4r=#!f*xoax20H{$_xCAu&2*!B&yP;QQr}{1=V2&%X{=dnAom%N~hm?SWaB z#*3Siz8)=P@*)i`7k}4Kmgk^I-T-9gzi0Br8HnD0pj&!-v!XJ0v?@3_3NK7$ZfA68 zG9WQBGdMU3FHB`_XLM*YATSCqOl59obZ8(kGc`6bm!XOQD1WpCP#o&AEeyfk-3E7e z_aMO?5@dh@1{vJlB{;#|65QPh7A$CRCpZL$K#)iF-uGmm`~Ow%)l|*Q>ec>rH!Y=_ zCaa{my(#FmJ;a5TlZ`_NAg!#y$pzrx;AZ3C;6kRQ)dIWNg8mUB)9QenoWb@Gp?@hz zJAr^MFESaR%YTcWvONT#=xPh#R`CD1( zZv_^>KfM8PvT^>$-9Og99f2W#83WDC?Cl(Y5Kl0~5?}$g1p!oFE3vtFxUc|#5cA)P zKwD?~7k_)88xU*@G<{L{D{~;=wWK-#_)_3MWDdo51~(0^Yf=Wp>#1-{-6_6`7xmm)yEU<=U8 z53;v2&kj3BlC*os?Do(5GyJm~jDY|4rDFdwv>*V(pBaD0 z!OLOx^2Pc8Jn4U#{QnL3Z!iD18UNo2y>_*={flP!i~s-7Ks&Il=RXQBGwbT|GXKi< zFIxcl->SNxzjsy{WDa(<``@gB3-DzRB!3~6w*P4(*!eZs17xlSb}_U1dszOKYyVy{ zTQCHqX73FCeN_OgoE#kgi+kBDGn<#o;rud@e^H>9#rf}$vJf+S^WPiB#mff(IynJ7 zkvU!liHnyP;LZ86ism4Xzh)S~&IYk}c`*UJl;;buuy;cK{i1xl0CvgWqQ4P8fPY>3 z4-x>d%lrrNa{}1q{vaLzyW$_j2VhtFgI>HS{|5=u9E3u*BO^1kF`0e1Vh`isQ!U+^WI z)xY3N*5H4^m&Vxs3%+D(_peYdd4ECvgnB6o^754b;$L#H|5MnDvHgG1FQJ|O1z$ot z|MB)hI=|e7KlDrIUH;H7pth^6+QOm2FvuYl$U_NAIK7288goB=z=RJ`ctT8dE#%$P1JBgA+wZ zlJx^*NTjS<61zT+jy^hpHZZNw-HNn%j;;deY6Uny-TUP{>Ice}$HL}z)OV}--k>~` zO|!;mN52dFG)tRrn*EKC3V+^(l?;&y`^*Dl_WEW9J9pxlLNS3E+4nexyV%=cgDdv# zo7ZZE7MF7m5e<<6AsNgy_Sh7ix72=$BEh_ON%6YVSn41b8b6&5 zE{y1CCxjS?2-#t972Zd$x5+2keWFZ+=gB@>QC#;>xZrO!b!Jgb1b;uP+Va=)YM==2 z^xo&zd+j4I;MM6dg=<*hCD&6x`gp_1pmXc#&?5?WEQcOq2-e;WnXQhj;x@JjqMOK) zwRQ$S8O@HQkBiKp;M;t?)oRyK+%H61jGGuI6w{wKTm|4BwKH!f0Gh*Oy z8@Eim>{+t9GEX22s|ZORAujf=`BnJlxGTQga%K7Ek@rppK)0)F5)#E6oPJQ5sUa#-q#Qz{PvI9(nIxI}&X)vsI?a~6f zUGNHD?74Pf#bJ+}Gf-2f2HH1nNa$nFS{d-toWrVr-rL__#18%&m;1?BZpwWaUZ^GS zpdU<7%9m3pBo5$@BX3Z9(=iTG`By%RtEbW7XY#!2H|U>bOnoY1CXTdI@2AHR{ijd-x0@-30@*9M-ob7OxOP-5h)t%S*MC0fWGQN(Q1wQvZ*5yKC%RrTmoevS zT6y@}ekAzz3fv4b?Z`gSM>x;Z7((7_JckcwEgHGnw=EYqbVD2+Z6!GILG&rC8=0kb z0yxQ2E(*_*9!z6}G@FzwmJ}K=gz^EJP^9I;8ofGDC)yhU$KsT>A7hjkE2g+Caa8x0 z*?(|Dwfgy!2=ANRCzAXb#am&Dr4iTjQglpl`Hi-0GIL6=6@@Fz(2!MnJ}_OPEyl=^ z_IxbbSoFzEob4lz&@Os58?Ra~*he2Ov7PnbOyZli%HV^0i^R3K-Z+^XPflU5;ii3+ zv1Qw#=0nS2GF(xYu<;zNtH^CIzRww>`3`=f~8%u^vC^g8aDXuYpCO4jmu78#1 z++o3K%OmDtlNSU-o)>3ig^_6^ajt1R7P~_sZEIu%KjypC#Of$0(KfXXdR{}4sBM4x zh>+Lp6XmWP<&(Yuev6*;=~y00%TY2a@>|=0Z+mmDCWiREi1Mo_DJKztz3_& zN!pv2*RDN$TWQjB?`eq#^tG<(HLjwEh^?oga_#3h$&||=zDiGV%ur`ZLVv1HjqGD4 zw-Uj7Gr2RdJ9tod!#pjq4dsf+=pj<5@5jE-l9o&E4TPv!X?I0FDzPYb2!dHfuNGMhJjq~$}M?hm*1DOK@i%uT(bJjQdIyn z1z#~1VoHi(QZFWR#{EXEZGXa*F(2L~eG1XhB=?Gm4U-mX6GK(0VNM?gAFsv&>)T!i z*~(){MM`dGAvh3|WWnJ|;TS5M!Bg26S?30osc5?fv&ph^Hn!zEG1TzS5i=KAV(Q)D zbgd*QLmpK-M`NvCkTmq}h%g)Jk{Sy~UDn0q$6!9kNP7gPGzVt38-Gh{TZ23&aw`{- zxUb^eLR`fzi|31;bR@6SDcwbG94SuGPiop>eNRS#x(+AYrz^(_7ucE2 z^{O4nma~r#>=~P-GJhhdybI94EW+^!_c5R}8o=msmj%vS)K32xZU);uXT|XFkg%Ip zsXQOk6Q|$6yjlLtz0-+xesNG6SgvX{p(r_&o>AW+X7mYL&@Fc+-jj(0fnLGRrDdi{ z$Xtc`G&QL3gVGGiE>OcGua7eH!JM*F0|oj+rhi06Bhz^ve1Bl~cfs8}0?n!c8FL() zg+Ag$Q<|E4o%5lvG9#B19$zb7P{NwLN`zCte5FD$i)u3hAN@u#(dGn}Z9zRnSSE7F zbC`k6W2M`KrL{%#XQD=v>ouQdhv9Z~p%TsS1sk4OAu-a>WMPi<M z5^91qE=fkIfq&Q7X;FUXpwiJY=uc-;8L0e0@ z;Tcd#Y9+>giYc!-&J>fXED~=#TCKL0)jiRwty{NXJAa8WBlO)Y(HM<`bY{lj5g8`) z7(cZKLD7*y`A}^*Xj~+qP{R9d~SV z{xgf&&idBA?y0)<6(rX(zWVh+50YON;@ly?FD{EIX5-a#N-%je`%i3!ze^e4%>lH_ z@~~ZMu~h`T*hxrf@GQ-l8A9Ev4G<=k`w}#4w835dCF{Xb1*_8`Ko*(;gYzaFn*LC; zR}ssR4zQA}XsAl>Ul(LAaM9s=&hJ3#LPE0Fj}N^k>bC-~K0GSpIpmN}AqL4H*W0+R zzQ;g{nb+ceRz!@2Vr&tvF`h6rrziR+4A1H)3NMfnPEiaU)_k^(-mMe#6F7Z8u3e8r zfEif^D zbFQ<`i<}1bvS_lEn*P+j@~v{l>lfSry}t|7p)9zW2~^tgMpa=4G*Gt}Z93-INfm;< zj3aV2>KH=LSiuztsB{b0A6VzLAa~_gq@f_aMbxgV`!{dR46t!TL+W1+!MnmBD? zW`%6wsl=PGx0MB<{~W3?fNRjp%X``gs}>(#voGcg0*M^w23o+|ro-HvKEc~VnbaW! z7CJi_kCTy~__{Yho}o1ILC>!-WWTaKes8@@d04}Qb9tN zTek-ap$ddwjXx>oWB7>dX#TE>(P+}TxQ6uaU(>2|K6Y>v=ePf{va^UHz)Bg&k?nAn zq&Q8+aTX^42TJnwb2wdL=T{$=kk)bsF?KAiDgc!TQJ)|5mR%M&>O2zTvHr0!96%!Q2n4Zjwjm#LfXY8+h z1VOK!^bCCLzc^}*Ldr9n9maicmfIu>qOK*k@UNIDxew2%v=<+ch^6&MhEw)S7bodZ)i!HER{JA{G{x-Gu z%g_j5r|5K_PaxdUZtjp^&KR=$Uld4Y@WwYGz+r>Bj*0k0<3P@xV^XAEwiW_vm=`I{ z+_k>}SP}!El-h~g2aVhfws`3sDyQ=oj{E@?j^^l_LY3H*(>0r>9Eb|4g;Dzncj^@J zuVbNt`biB(qNy?R&l~LI)JFPF#sdi<>$tK1gEa|K_HD;R7*B7(^e!8{@_~gnC6me1K_X_InMYMld0>4j7ugJ0Ngm3{Fl6$dXGa+RqN|Oc;+Z_#&Tn2{~-Y&(*W!zw8_Et_PS;s?z;fmLm z>SS8;-qJ3$Gdph=U^IJD#&OGVU|vp4$nS{e{_}AZ1!)|-%>2}#0P$>8joWn+;B;BM z085{k3$LcCb9|?Ymmp9tK@3@ky|-hSs3_FvrYYrh@pqRJw9~ms+@Kl~mez}&lTY1y z)bt6acnG5=Cao93yUFQ$lU5)o;SO83rHo*&wo<762W3}Z-Gk!X-DNa-sF{Jt?0Aww zZ0^l$lojgNZCkBlLYF3l59h2HFvQt17xg9*rkEL{;2sD3Y}My7ncNCs*!D2miORM+ zAmjd`5UY2~FRgimN#{4nq>*fW+}=CTIlFw@Mc?dmf{qkhBkruaWjvfv<-fkGOkO-v z-o#QIqv{6x7-zGyLDk%H8s99kJ~@VkpF4%PV_SS{ml)2HYgxPHA4=Z?TplUeqBjaI z>iys&R!Tw@e2L=C`(w_NYlCf@txuP7bqwWWEox2U$6{(Z+D(kYgSmsX2r`Bt;Jl{Q zQY1IHu!-uPCH!OmIfdT+MKW@4c3GmAh2%^t+pxM7EBiV)!fQBA`}IflQf1BfK~woe zpcNeZaMTLx{ZCxp3QigmkXrbdhjqr#?*25CZ+hvdAl}+1sX>xEMGg`mk*EviTr?C; zn0CJiA0I$fX(AN6m2hI^_-2!3(19FoWd6Qs$%VYZ>{pt?d7Y&#UkKo;RF(~Sb5FVP zgL3|0_+kRHe*&#FbFi^v#<}Fj%Gb2Ny_}~{ZBL<2wwO_SUa6|{$%TP zX!@v2>j>|Bq@*(;-;rz(K)2vdmhtQ1L3nKdZ_1kptS0fJq7g;@^2h_v^X=9vx1y0f z3nw5W&uCE?I!Q}Wz!KIOEw_@=&$-fGr<-m0{w&Y=So&piUOjek8@(n%F|Z>z{#P9u zwBnZOu9?u&7)hWx&_wImT>^>IV*3L7E;4l9+#>Bwxd5?r3DX4*hZ(NvObl0$QWll#<{a~LMA}3St?u1-8<48reee3!|_(P3BC znY9Wci&)vNL!{?KEU`zy*Jm;`;m2+CmjtDBXt3=E@PQ}C z8^M9==FWUL9d$4;!)sHp50Jc9GhjBRAmjIOr;%4!ayZ1P0*PBS0j}Zdd=wV^a0fAY zpja(bvWCYuso*Pp!lAXw+ID=cPI9o_I+>0RrNf2Zng!31mP=uZKKQ`=jmrDMjp&3j zOGpm_gc`Q7#w||hLH&i;(ex49RjTuFZp9is|JBr0NuM-1ujqA) z_cj*`#;H3bfv&av57K~{>Ex@=Odk`X3DVlUpB+dVpFeaOE#tW+Y_^_cSJ{=TsGZ-a zaakV@efcqvV2QeBgx>~dl_!##5+P#e_-j24=#Iu$*FiC~nIWh#7L3XGiIh?w*TtDy zvn34{#Jne;Ye-$wGtaPHeSI(9nDf<2H)+^|fIsEUQZyh%ESVE_FjdXhFc2E+4yT>w(UN(2A7UJay}tdfluA zvW}A-5dVf!V7m%L!m+%|0#}lW<^P)4m@;XW5NcIYRy;_*B^37p%e3-ZEz6!vnbz#< z28ha0nV^SkWC!r)#myM`Np~OJ2nnt2jbfy##h@9+!2LLf1Pob0;WiRc^CuFSVN3l; z_;CqH8oqKVx1^BiY&WjX7-hbL`5;^a!`D@Z&}?dI>bXL)6ERUhA-M`)HV^~=_foqJ zY4remL;t_Ij%GNY;bx#V7_Qg9^*k=!bqOwQ@3-9BpN*6yf2dDes9W_3~Poa-zt$^crpY>7Hxa{WVzC5nQ2*V@kds~V)Zg#L2GKi69 zi-cy6q~_`c$Q7nZ9-Cn6I}Cc%f8XUg{#)smfwbxk~dC8K( zT3_I{w_$t|r_5L(w=qB(QZU{JaqPHVrQjTn&%Qv~5#Ja92XDM=@9JP4N8!jA+MECZ zhrKuCdlu!gYPgq`lDk&ab|C`Tu6~??G+}lHR`q{{JceT)zsIbMW(0+R=DMSWm00u{ z+W=%-Vizgkx`oDjuhi=oR0yc%Ao2!AMn^=8<3P4x9x@d78b5ZOmE$dHlLA@%f!I5T=GNJ z8P4CJAC<4cmFsB zocf3jGN^N=mxTCLY)6vvm5;D0!dcFJ|}^CPISA z$qIV2hV)D|a~%fkCd)Q?TtsTO4y`sM^LzmLB3uc3r?cDgmJg+>6sZ> zP1z*7*5YX-mA0?UGXl*|QXZ2C*0jxMHLAZf3raonu}y zA{ZI|NsmK%k8f=A3g$5z17!ce49x5V(-pt1NoVfG<$7><9g zDNG3-7qDJHRq*)~LNG))Vw&Os&+JP)J#3}VE5mTr%Y!5*aDsv+5Et@`I5Snz%!b`E z*_>;5E2^7VS$*Yi=LRD-E2?hSf+%I@?D;SQ5r9ZZNh{9@KPvJiuEer9z}aM;t3Kyu zF}oc|yL>=!&4yIw?&Z|ckhxo=f=VTW(OvXW@M{v7tsXmkl&hR?ZZ8*&JNKXW`i`n3 zmmG&SNF9p#?ea=HH8pJgxEg<0eO;){%wGSUktxsIqbS&>l1YkTj%=CW;LDwPoQ}i9 zU4w3Zn@{ekdALj==Nk_jcca>|%I7MhkT+i-_{B&cknYS{I{&7+C-}P!ls!2=U;4~9 z9asn?*9cVxcf~7VNLc=zozhDoE?grjSEvlyu=WrrbGK&J_Il=jELy;N3@YC`?kHJf z|95VFmO_Bv0xY8UwA+GIDsRab5A(jQ;q#uLaJu$8l_G6=20k<7@q7(2zDCMQI);}N zsqR#Ztm7xUo~AOQ`J5G{?C6(X-UPOn=odRs=3b%Fu{7~~pk?=HK5uxSpXlg< z(P(O~9uM!|%Pt_vr-!0p?SscSpWDG#^XwTxw{MdMMF?D{9&MzOv6qCX=PWE=<3ax2 z;r(MEJMok5fHj*Kv!BdF;k=Q+4Pmf_s412hb~)7ngT-(<^G(4s#zXSr96v4PJ{t#^ zy!g>zZX-|U$R}izRM@9IoG>NZp6G_vr}SWxG{;T%eGoOYnSA0k&3sL{p%^e!T*`87 z2IpH8PEhXPIEjCr_3{eTitUj^s5M)HVhPVR5s-&T3a(6B)VPIqkw7Qr7fY>~%1%0m zjm`@Ld1tZ(!=NJk{YZyyLA{&m0Fw-arXG)oDfV~m^2N~Q{aOsZ^8YU?)h--6^HO4* zG2%fhq7v?5w>m=Tb#o#J1|IW*fFByv>ovz*stLf5;^fqR>n|3Xv$ozb$K*@kZWxW* zyh$DKNI$)os+>Oh^ok6CMf;=YKWdj%I+8bX-3&+zBL@v?)uq-gL=<3KRM-F>o9(mk zQ2Q=V6P`whG(K?#Bv#$5#+wo~n%7p>ohV&Cnauu?YscxVdo%MG@^vq}OLtt1?z!?Z z-MI6OyX8ZL9=9A`f1ap@^~RV`>*HGX?Xrx$F(5>%HQD}G`;b2}xcf6wr&zm-1;N-( zF#I1AQrZ2wrt!2H-N}OE7l0brI2pEfXclvRMRsG&czKCuRXEfFXoJ!&HaOB3oncR@ zUX`p{f?zAHhet?V&pTOvFN9e&f%Y7Hey0j5eM+&t&55E8ms`WHy5iK!Ox0=WtvKR6r@jn=OI4@3|3i?L_hwQad|mX3 zv=Jf7Ra6Lx$cukYOy{XNx}Ol5vwfUjcp>3t@?1+f zpJii)jXmW{T=V9&9}wfhX*mi0CZ&cyfdJ!Ui!tubk}va>odsl(&bfVwggTZQ%!Mnn z&N+U1KT905uH=eHRvlijfU|xek!2Qs)6Q4T;VjB9cl=M}2_NP#NX<@6>AjghMIL!q z!8XNPkBByD?|Q(+S5-H&GtM+#S-Q3S>4la~!D@LI zxX3RZ6hM&}0sQDpTqXAyY^2v#JO3(qmt}W?mLnL(kx08JNnai*+hK4LfycV{s13dO zL^5&W=Q0ERt7q04xGa0_rcRI$0#7xXboq|}YOm@uD;ID-vY@fzZ@~hO_kVmUJ%Uy? zYXz#FHRU!Y1iXqRtTB<*4vF3hoxNsjCRq%|L0UBsWIT#`OD>;%mN{Lu$0GZ9(+%IPM9H;sGmqzFDj(Y>G8MB*Akg7-hGKqNCVfB;V344uYG zWBr6#N{_Fm#?V8b=;Md=kZi5|wK7ek$;N6GCZ-?Y$@k#cVqU7-2 z$S1+KCBB0K3LMfJUIV)gt6A?Pg2#co3PSR!Hxp^)@GPV!^|E=3JI#Kg4Q}SKDwi@O zaWJU7rT$g-5rV~@#hGRU_XvFzE%#nTjO&RtUJDkQFM=R!?-^>mW3IjLYLvV)g+54< zW?6HqzM}m$Jh7fYPgitZlg0&QHk}bYWT8%K+8a0iOex1t>kKx-JCP#9e+xj3`9gly zFxQ$d{(bIN&z*dAJTUsHpg-l;Au-!dJ7g7%F-dV_44S`!*WzQwbgEV4tzL+3NVY*@ zq=5zr6mIT~6r?|RH?Ejux=g_%APsvpjer`#^2&82%$e~Wxt6B)6wA0R+YJ80SaM1U z6t$?57!p4fJ@r>oLN0M0mro#!e-v~JK_hR7}gclL0Ri^8) zXRwuq%WU{=xHE~M7@DebMB$MH=v*W@X9YwlkPxgx#q#w#c>i?ez^(TRANQ* zhsIC6_Atqv=$l`*k+(~OwTBKe$$<-(ix^V*A7MLw5SgbFzF(5|qNUr%I~`RMy~8+5 zzuFvwdJ#(N{^v^KvhGK@?MIvZ+?7rly~ZzF2jk{go^Bn{F-st{XS6=5;h0?$14*?e z;dRh|=`x=yGuzJ#M;4UXx;0LQ228c+$=tIJx@jGO{*3EK>1oqOn!CJ5GtLBp#M8BWT#p|6`k} zfAE$m%Um}Z=<)%p`404DoyDY{&Nwn#|5iikC*&1jp+0jNuVjg=M2!^If5pA=*ePAL zk!nTmw&88V42vQVOJN!DTxQG#z(f8v#;iCwi`vnG*+GO>5M1M2!SQ#;LmthpugYx3 zKqKvkA@QnA2@mKwYwj+lLEZvu>$oM?Xq>c0k^SnR5}*Y-ACK9u5quJ_!Uk+B^yT+F z9}#yhr(>ge*C0esixbDcHx4j17}-^J z`#)TdQS1O2QLDsjc0wArVH%)KsJ0RVBz%-5$7FN)IH+3H4WuJ*Z`(zc zP_Rhca_yc;-M(gfj-NWUNXR)e{*-@K6r=}qVbuqIk-$hCQW<;L$~u}Q&QWSuTcrM* zb&4v(7D>`3jkI(q@s(%PIMkEJ@73sqjeBY zq%k)oLuNr8s4^gm=$lw?6PJ-lZ>F&APr-}+9*-KGH_^6sau*0QE1xR@_G%!WIy)Py zcBHx6#O!8b@Ad6$U|~0=wl$koT!k!Vv1w28-72yH{mM~P^&Sl$0)MjP&9Izxx!JVK zD;a?dQZ^+*-4P+`88&+fSptjmmR-h;ub5kSc?-&{zvx^Pr6loKzl`=Va`p9`nEm4~ z_3}kc*`6A$a*bTmMCNOU-^M$7g|{Wd@Rpau>{e9qX$uG@0QfmRsg~1Or4b;l83QB3 z+!)Jc`yo1i{{-Sdv8ftc^YoB!iaYO)1oi;AXdSY`xDNX=V6JtDvdZxAmKAQC=b=d} z5wDqlidmKm7hbQoo>D%3p_7p*8-82ngw{DDRvtzA!9dN#D zO5Gh7iAdEkM(6TM^)c(ZR}=q;iiJVb9W-$DSYOgRaYIcmFvK===S6$>;?s`fCwrzA zr33({4g@sU8L2-h*}@_Naq(~u1+IX~O2B$?He0Gx@TVM=I>a_su|lN?u_hmSn`}ts zVDWkABu~Eaz}xk5?QKo0b7w0U>+W>G{ySv8DlthN1lZXYAlP zh+f&nRQGPRWt(A(X9LWQnGTq@J9qi@`wXvA^v{^rRbBVR+s6CY*Vjc|7y=g86(=tC zWa4p>vOs?OyYWnYhyv^B4IU@&n0QS^QP6HzfZNsiTqAQCJJ!7W@_{%m!%mNMWD3qr zZ+9iK2o$XXZ|@a~E!n;eW8^q+-jD{eB2H|+au&h7js?*^a0sIV07)jSB4^Z@nhgfW%W_%^QJHG7 zO6Hc+ECTRTQ-A5)7-or_Es+GnKE!`dJbYYgY{swh?*<&Ju^ClOO=_thoGEhefceH^|q-P~Cl`m%pEMC;Q5wKZh}?UwMLgTKwJkPg4pQJ*CoF z?P_jMT*hL}-Kr&rPK&1b{gD`w@lKkF<@HDp;dG811Y$b5KFO8p}Y z+_qa(Pul1E^O}!q!v4501{2J ztS+}4X8a}2+=`V09_pWTv-l32NcvfN4%D6_zpE&0H-YYH!@IEGMAmB8#ke65v-kyV z5Xd!@AqVl(kC;sO)|r!BQ8{u`B~7Ulp>vFuVY#v!273}*fxSxE2qqOSc*fhAoVGm0 z2)&oDS|9@S{VyhszdIMxin>975^pvslf_FPAxuAxuduWFVpON_c@y&k;*~Hs_=%N8 zi?!52cb_}>G+y5``xbx`A>M)uUM(6*BAOwZIcaz(*Lj?AFK8`ZqV8jkf3@bp?Ki)t zcuUM`!)MiHSDBlknX~Y-PiY(Gm-4U?e=lcN3Q&l*-Uj|*`s0;u&7Gm=8bW(OHly|vbUOb8^luL<)ZU^_STZt)FUUDr|CqqXEUX7H*{m$sFDT8-I zZV2M@gSY{zT;V3d`X4yughDhXgZ2Yl+X3W;hG~N5T^1^|n!k+Mv*3C$_lIOQjg{mU zJa8+P$$)NkO_yarDaSJsnH+b#2phV}_+rmqXAU<1^nKZYhEh#Mxl2=s9zM8-5=G_twC7X-1TsGhR>SS+b%;aAxa$Nr zahK+C)?FQ%qoAGaF{hT~C$hRo*005w4S0LzC(GK{bwDYf^kGAPkIsW@qX8>(bjB8r zMSW(eWz8`PcgG-XU}~B=5!u)1#tFp=B>6cl#pO=oRsfRtTwaDU6@*hM*J%pxM~Y~~ zisSNUS;?h4OcM^GNPO1ZxB5br-!7rMds;#}Y23|W$m))gjHkh;MCczWDlPa6avVJ{@D_eMpe zG28wPLt1_L0g1(+ylt~309{xCvE*m8oVreHPt5NZ8fALL zkPdan$w^^Bt~x0pSs_SU5wdH^g{&8iG#lehSvueX8ST3*M%k>y458Y)qo)o1ZZO!P zcpzMjyz@oIL&E)CCG31OSSjm6Azdq26#fX-gJlQSxBNli`IXNx-E8{%7yhr3`Ds{R zu?}+jlq(~ET>m-6*=@`SZVa*!Sw8Oe*uttMCA?%k~%qoe-N zsli@!kQ=F78-hlIbL>N?%P>=!iJzM+c?mTe*Tj6s=T8w_S3eXe5s4 ztWs6AX_BO4sLr?IUfy2yPu>PE%&lgDuQm(bu1ROha-;jqdukTdBCyB&(~+l-q$yVb zcy_nM?16b$2eqaiccVD^U@efxY6hquGtg|B_if`?7Rx04$p;+kG@65|KoTuaA`)Bnkuz zMoaUEa}6QQjGEA!texFM_6A4~ z%U1sJR%secRio;eyRC<6_wo}d=Ksq^^1lAi2dWl2G<>(2IywBZ z+^>!|z+@E3FFE(QAtIB>l7e^x#(NHww%M4J#U{85z0Z0!bmB8>_#0rgoe$({qs?^l zz(Vg!ztML4=0v&N_zCMi&bCDRb%vBAUaKzTn^$;|h)`db2XeKr4xAjH7RmHlRSuE5T*e}E{%2u9{-5=Y`j#^#q$wj1(Q!L5?vQ}`@Vm@soViiQ+~>&azP zY>7a`W*8}8R&n!_XD2+YyJ-W}ks%~6Yl8f$VPMbQLwB4+l8}=KoKs7%Ia5s*jfU9} z3pf}k)$TyZ^meYGnG*5AlYnp&Cc>RdPrfTJvFu64Rpl?OCJT|vpmVFz0FZfQl&+>O z(wZH!RXD`8?A3s`o)u{*nbwS=KkA+kvCY8C{2r-JHqnzl8&5P6I@U~FVp82+2V{}T zXpYJnWd3gc2Vsy11TH$T=ctr>d&yc*RoEk3t?FPMk6w4K2b4L-=H|4sYnaP#m<#={X3my?{ z8#ClQAcgTl7LpgLlTedv0Z^_iIRMrYY)}Pxob&RqpweA@G1xU$?iEAcd_|BYna!fw z>?x@9TO~vnr=c6>#x1+o6PYpj{q}2^KzkY^Kn(~RAP92*|YnMS%p zKxu+p(MF-J;<3aD%tNv13%iVb9B$L#+RX@U9OvB$Q;3jX;8V zM9-sRg9+hQ!H@Ytp(%~s;?Uk@A`z=@@X`0)wKivWuLwJjfS{U}t<>V)!MpeS7loMPmh&_Ck0@r`!e{5qOWzx?pI2hj69}_tbAHC_ zt>%~gZ1Wr0-zzI)K0a2^E_6*wv$6O8XmP z45{SJwi^|H=F4`$?=6Zw-Syo!39!i&%VOqF0o?sf)lt7dac`(j z*5SUD==ZziqO7ln*9R#5oMS(|#)1rL!`Xx9&UjI*+ZdbK;wDpZ)#SzxpVpIge=eg! zn3Ar?JKCz~?_YJ~;p_hRao1&_fMh_t^swMPX!FBSK=-)Fj9F|1H4tG)@|laS3mpP zK(EcN{-4@qKC8it8h(tn$l-swU?rcU#(`GLd^wf%C0uH~VCpsQ(m*5H@B%YrgI#$7 zz2m{NW2e+0TQ*7WenluDb*E$DYizqVpo8#NRY0{KK57fr_oVBxc7lKRQb;>h#NzKJ z+#aqjXp+lCAP}+6{7DgE(q{j@mAP;GoMI*Ip``YLx8@gk z@Q8*LDM4mzEO*ZHv?=dL^!O9__j7q)IPres>r52%9@4;L$!D2F7c zI4`d#r=%#mgs3<>3kNF?H-|VIuPB=ki2%?4SEBcSJP0-}_B7H-clcYVVX7?v@uAI!}jq9;s*xF`iCmOO<3$n}e@&>PiDwh#8& z^d&P}l-zpW~H0Dn0pF-;bM|pZ{rcG7?)~wK1-GtvR{#DKp`oTfb@XEm>26PTyw+@FK7OuxKLi zIju^1>x=2x0pFbhb`#NW!K