Parameter Sweeps and Policy Comparison¶
The Python API makes it easy to run batch simulations, sweep parameters, and
compare scheduling policies programmatically. Because each Engine is an
independent object, you can run hundreds of simulations in a plain loop without
any global state or process isolation.
Prerequisites¶
This tutorial assumes familiarity with the basics covered in Python Quick Start and Trace Analysis and Metrics.
Simulation state is not reusable
An Engine that has been run cannot be reset and run again. Always
create a fresh ``Engine`` (and a fresh ``Platform``, ``Scheduler``, etc.)
for each simulation run. The helper function pattern below enforces this
naturally.
Helper Function¶
Encapsulate the full simulation setup in a single function that accepts the parameters you want to vary and returns a metrics object. This keeps each run isolated and the call site concise.
import pyschedsim as sim
def run_simulation(
utilizations, # list of per-task utilizations, e.g. [0.3, 0.25, 0.2, 0.15]
reclamation="grub", # "grub" or "cash"
dvfs_policy=None, # None, "pa", "ffa", or "csf"
cooldown=0.001, # DVFS cooldown in seconds
cstate=1, # target C-state for DPM
seed=42,
):
"""Run one simulation and return the SimulationMetrics."""
# ── Fresh engine and platform every call ──────────────────────────────
engine = sim.Engine()
platform = engine.platform
cpu = platform.add_processor_type("cpu", 1.0)
clock = platform.add_clock_domain(500.0, 2000.0)
power = platform.add_power_domain([
(0, 0, 0.0, 100.0),
(1, 0, 0.001, 10.0),
])
for _ in range(4):
platform.add_processor(cpu, clock, power)
# ── Generate scenario from per-task utilizations ──────────────────────
# Each value must be in (0, 1]. Total may exceed 1 (multi-processor).
scenario = sim.from_utilizations(utilizations, seed=seed)
tasks = sim.inject_scenario(engine, scenario)
# Enable energy tracking before finalize (required for energy metrics)
if dvfs_policy is not None:
engine.enable_energy_tracking(True)
platform.finalize()
# Schedule job arrivals after inject and finalize
for i, tp in enumerate(scenario.tasks):
sim.schedule_arrivals(engine, tasks[i], tp.jobs)
# ── Scheduler ─────────────────────────────────────────────────────────
procs = sim.get_all_processors(engine)
scheduler = sim.EdfScheduler(engine, procs)
for task in tasks:
scheduler.add_server(task)
# Reclamation policy (always required before DVFS)
if reclamation == "grub":
scheduler.enable_grub()
elif reclamation == "cash":
scheduler.enable_cash()
# Optional DVFS/DPM policy
if dvfs_policy == "pa":
scheduler.enable_power_aware_dvfs(cooldown)
scheduler.enable_basic_dpm(cstate)
elif dvfs_policy == "ffa":
scheduler.enable_ffa(cooldown, cstate)
elif dvfs_policy == "csf":
scheduler.enable_csf(cooldown, cstate)
allocator = sim.SingleSchedulerAllocator(engine, scheduler)
# ── Run ───────────────────────────────────────────────────────────────
writer = sim.MemoryTraceWriter()
engine.set_trace_writer(writer)
engine.run()
return writer.compute_metrics()
Note
sim.from_utilizations() generates a ScenarioData object with
log-uniform periods and Weibull-distributed job execution times. Each element
of utilizations must be in (0, 1]. The total utilization across all
tasks may exceed 1.0 for multi-processor experiments.
Utilization Sweep¶
Vary total utilization from lightly loaded to near-capacity and observe how deadline miss rate evolves. The four-core platform has a capacity of 4.0; here we sweep from 1.0 to 3.95.
# Distribute total utilization evenly across 8 tasks.
# Each individual task utilization stays below 1.0.
total_utils = [1.0, 2.0, 3.0, 3.5, 3.8, 3.9, 3.95]
n_tasks = 8
print(f"{'Total U':>8} {'Completed':>10} {'Misses':>8} {'Miss rate':>10}")
print("-" * 42)
for total_u in total_utils:
per_task_u = total_u / n_tasks
utils = [per_task_u] * n_tasks
metrics = run_simulation(utils, reclamation="grub", seed=0)
miss_rate = (metrics.deadline_misses / metrics.total_jobs
if metrics.total_jobs > 0 else 0.0)
print(f"{total_u:>8.2f} "
f"{metrics.completed_jobs:>5}/{metrics.total_jobs:<5} "
f"{metrics.deadline_misses:>8} "
f"{miss_rate:>9.2%}")
Policy Comparison¶
For a fixed workload at moderate load, compare GRUB and CASH side by side.
n_tasks = 6
total_u = 3.0
per_task_u = total_u / n_tasks
utils = [per_task_u] * n_tasks
results = {}
for policy in ("grub", "cash"):
results[policy] = run_simulation(utils, reclamation=policy, seed=42)
print(f"{'Policy':>6} {'Completed':>10} {'Misses':>8} {'Preemptions':>12}")
print("-" * 44)
for policy, m in results.items():
print(f"{policy:>6} "
f"{m.completed_jobs:>5}/{m.total_jobs:<5} "
f"{m.deadline_misses:>8} "
f"{m.preemptions:>12}")
DVFS Policy Comparison¶
Enable energy tracking and compare four energy management strategies for the
same workload. The dvfs_policy=None baseline uses GRUB only, with no
frequency scaling.
n_tasks = 6
utils = [0.45] * n_tasks # total U = 2.7 on a 4-core platform
dvfs_configs = [
("No DVFS", None),
("Power-Aware", "pa"),
("FFA", "ffa"),
("CSF", "csf"),
]
print(f"{'Policy':>14} {'Energy (mJ)':>12} {'Misses':>8} {'Freq changes':>13}")
print("-" * 54)
for label, dvfs in dvfs_configs:
m = run_simulation(
utils,
reclamation="grub",
dvfs_policy=dvfs,
cooldown=0.001,
seed=7,
)
print(f"{label:>14} "
f"{m.total_energy_mj:>12.2f} "
f"{m.deadline_misses:>8} "
f"{m.frequency_requests:>13}")
Note
total_energy_mj is 0.0 unless engine.enable_energy_tracking(True)
is called before platform.finalize(). The helper function above handles
this automatically when dvfs_policy is not None.
Collecting Results¶
For larger sweeps it is convenient to accumulate results in a list of dicts and format the table once at the end.
rows = []
seeds = range(5) # 5 independent runs per configuration
utils = [0.4] * 8 # 8 tasks, total U = 3.2
for seed in seeds:
for dvfs in (None, "ffa", "csf"):
m = run_simulation(utils, reclamation="grub",
dvfs_policy=dvfs, seed=seed)
rows.append({
"seed": seed,
"dvfs": dvfs or "none",
"misses": m.deadline_misses,
"energy_mj": m.total_energy_mj,
"preemptions": m.preemptions,
})
# Print a summary table
header = f"{'seed':>5} {'dvfs':>6} {'misses':>7} {'energy_mJ':>10} {'preempts':>9}"
print(header)
print("-" * len(header))
for r in rows:
print(f"{r['seed']:>5} {r['dvfs']:>6} {r['misses']:>7} "
f"{r['energy_mj']:>10.2f} {r['preemptions']:>9}")
Complete Example¶
A self-contained utilization sweep that compares GRUB with FFA and CSF, printing a formatted table for each total utilization level.
import pyschedsim as sim
def run_simulation(utilizations, reclamation="grub", dvfs_policy=None,
cooldown=0.001, cstate=1, seed=42):
engine = sim.Engine()
platform = engine.platform
cpu = platform.add_processor_type("cpu", 1.0)
clock = platform.add_clock_domain(500.0, 2000.0)
power = platform.add_power_domain([
(0, 0, 0.0, 100.0), (1, 0, 0.001, 10.0)
])
for _ in range(4):
platform.add_processor(cpu, clock, power)
scenario = sim.from_utilizations(utilizations, seed=seed)
tasks = sim.inject_scenario(engine, scenario)
if dvfs_policy is not None:
engine.enable_energy_tracking(True)
platform.finalize()
for i, tp in enumerate(scenario.tasks):
sim.schedule_arrivals(engine, tasks[i], tp.jobs)
procs = sim.get_all_processors(engine)
scheduler = sim.EdfScheduler(engine, procs)
for task in tasks:
scheduler.add_server(task)
if reclamation == "grub":
scheduler.enable_grub()
else:
scheduler.enable_cash()
if dvfs_policy == "pa":
scheduler.enable_power_aware_dvfs(cooldown)
scheduler.enable_basic_dpm(cstate)
elif dvfs_policy == "ffa":
scheduler.enable_ffa(cooldown, cstate)
elif dvfs_policy == "csf":
scheduler.enable_csf(cooldown, cstate)
allocator = sim.SingleSchedulerAllocator(engine, scheduler)
writer = sim.MemoryTraceWriter()
engine.set_trace_writer(writer)
engine.run()
return writer.compute_metrics()
total_utils = [1.0, 2.0, 3.0, 3.5, 3.8, 3.9]
n_tasks = 8
dvfs_labels = [("GRUB", None),
("GRUB+FFA", "ffa"),
("GRUB+CSF", "csf")]
# Header
col = 12
header = f"{'Total U':>8}" + "".join(
f" {lbl+' misses':>{col}} {lbl+' mJ':>{col}}"
for lbl, _ in dvfs_labels
)
print(header)
print("-" * len(header))
for total_u in total_utils:
per_task_u = total_u / n_tasks
utils = [per_task_u] * n_tasks
row = f"{total_u:>8.2f}"
for label, dvfs in dvfs_labels:
m = run_simulation(utils, dvfs_policy=dvfs, seed=0)
miss_rate = (m.deadline_misses / m.total_jobs
if m.total_jobs > 0 else 0.0)
row += f" {miss_rate:>{col}.2%} {m.total_energy_mj:>{col}.1f}"
print(row)
Next Steps¶
Visualizing Simulation Results — plot sweep results with matplotlib
Multi-Cluster Allocation — heterogeneous big.LITTLE platforms
DVFS and Energy Management — DVFS and DPM policy details