sched-sim 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sched_sim/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """sched-sim: Discrete-Event Simulator for LLM Serving Schedulers."""
2
+
3
+ __version__ = "0.2.0"
sched_sim/cli.py ADDED
@@ -0,0 +1,601 @@
1
+ """Command Line Interface (CLI) for sched-sim."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from sched_sim.core.event_engine import Simulator
12
+ from sched_sim.hardware.latency_model import RooflineLatencyModel
13
+ from sched_sim.hardware.presets import PRESETS, get_hardware_profile
14
+ from sched_sim.hardware.profile import HardwareProfile
15
+ from sched_sim.memory.block_manager import BlockManager
16
+ from sched_sim.memory.preemption import PreemptionHandler, PreemptionMode
17
+ from sched_sim.schedulers.base import BaseScheduler
18
+ from sched_sim.schedulers.cache_aware import CacheAwareScheduler
19
+ from sched_sim.schedulers.chunked import ChunkedPrefillScheduler
20
+ from sched_sim.schedulers.disaggregated import DisaggregatedScheduler
21
+ from sched_sim.schedulers.fcfs import FCFSScheduler
22
+ from sched_sim.schedulers.sjf import SJFScheduler
23
+ from sched_sim.schedulers.slo_aware import SLOAwareScheduler
24
+ from sched_sim.visualizer.exporter import export_metrics_json, export_metrics_markdown
25
+ from sched_sim.visualizer.gantt import print_gantt_timeline
26
+ from sched_sim.visualizer.perfetto import export_perfetto_trace
27
+ from sched_sim.visualizer.terminal import (
28
+ print_comparison_table,
29
+ print_single_run_summary,
30
+ )
31
+ from sched_sim.workloads.distributions import DISTRIBUTIONS
32
+ from sched_sim.workloads.generator import generate_workload
33
+ from sched_sim.workloads.trace_loader import load_trace_file
34
+
35
+ app = typer.Typer(
36
+ name="sched-sim",
37
+ help="High-fidelity discrete-event simulator for LLM serving schedulers.",
38
+ add_completion=False,
39
+ )
40
+ console = Console()
41
+
42
+
43
+ def create_scheduler(
44
+ name: str,
45
+ hardware: HardwareProfile,
46
+ preemption_mode: PreemptionMode = PreemptionMode.RECOMPUTE,
47
+ token_budget: int = 512,
48
+ max_batch_size: int = 32,
49
+ enable_prefix_caching: bool = False,
50
+ ) -> BaseScheduler:
51
+ """Factory creating configured scheduler instance."""
52
+ key = name.lower().replace("-", "").replace("_", "")
53
+ is_cache_aware = "cache" in key or "prefix" in key or "radix" in key
54
+ use_prefix_cache = enable_prefix_caching or is_cache_aware
55
+
56
+ block_manager = BlockManager(hardware, enable_prefix_caching=use_prefix_cache)
57
+ latency_model = RooflineLatencyModel(hardware)
58
+ preemption_handler = PreemptionHandler(
59
+ block_manager, latency_model, preemption_mode
60
+ )
61
+
62
+ if is_cache_aware:
63
+ return CacheAwareScheduler(
64
+ block_manager, latency_model, preemption_handler, max_batch_size
65
+ )
66
+ elif "chunk" in key:
67
+ return ChunkedPrefillScheduler(
68
+ block_manager,
69
+ latency_model,
70
+ preemption_handler,
71
+ max_batch_size=max_batch_size,
72
+ max_token_budget=token_budget,
73
+ )
74
+ elif "sjf" in key or "shortest" in key:
75
+ return SJFScheduler(
76
+ block_manager, latency_model, preemption_handler, max_batch_size
77
+ )
78
+ elif "slo" in key or "deadline" in key:
79
+ return SLOAwareScheduler(
80
+ block_manager, latency_model, preemption_handler, max_batch_size
81
+ )
82
+ elif "disagg" in key or "distserve" in key or "splitwise" in key:
83
+ return DisaggregatedScheduler(
84
+ block_manager, latency_model, preemption_handler, max_batch_size
85
+ )
86
+ else:
87
+ return FCFSScheduler(
88
+ block_manager, latency_model, preemption_handler, max_batch_size
89
+ )
90
+
91
+
92
+ @app.command()
93
+ def run(
94
+ scheduler: Annotated[
95
+ str,
96
+ typer.Option(
97
+ "--scheduler",
98
+ "-s",
99
+ help="Scheduler policy: fcfs, sjf, chunked, slo, disaggregated, cache_aware",
100
+ ),
101
+ ] = "chunked",
102
+ workload: Annotated[
103
+ str,
104
+ typer.Option(
105
+ "--workload", "-w", help="Workload type: chatbot, rag, codegen, mixed"
106
+ ),
107
+ ] = "chatbot",
108
+ hardware: Annotated[
109
+ str,
110
+ typer.Option(
111
+ "--hardware",
112
+ "-h",
113
+ help="Hardware preset: a100, h100, t4, rtx3050, or path to custom JSON",
114
+ ),
115
+ ] = "a100",
116
+ rate: Annotated[
117
+ float, typer.Option("--rate", "-r", help="Arrival rate in requests/sec")
118
+ ] = 10.0,
119
+ duration: Annotated[
120
+ float, typer.Option("--duration", "-d", help="Simulation duration in seconds")
121
+ ] = 10.0,
122
+ pattern: Annotated[
123
+ str,
124
+ typer.Option("--pattern", help="Arrival pattern: poisson, bursty, constant"),
125
+ ] = "poisson",
126
+ preemption: Annotated[
127
+ str, typer.Option("--preemption", help="Preemption mode: recompute, swap")
128
+ ] = "recompute",
129
+ token_budget: Annotated[
130
+ int,
131
+ typer.Option(
132
+ "--token-budget", help="Token budget per step for chunked prefill"
133
+ ),
134
+ ] = 512,
135
+ seed: Annotated[
136
+ int, typer.Option("--seed", help="Random seed for deterministic simulation")
137
+ ] = 42,
138
+ enable_prefix_cache: Annotated[
139
+ bool,
140
+ typer.Option(
141
+ "--prefix-cache/--no-prefix-cache",
142
+ help="Enable Radix prefix caching in KV-cache block manager",
143
+ ),
144
+ ] = False,
145
+ gantt: Annotated[
146
+ bool,
147
+ typer.Option(
148
+ "--gantt/--no-gantt", help="Display ASCII Gantt execution timeline"
149
+ ),
150
+ ] = True,
151
+ output: Annotated[
152
+ Path | None,
153
+ typer.Option("--output", "-o", help="Optional output path (.json or .md)"),
154
+ ] = None,
155
+ trace_out: Annotated[
156
+ Path | None,
157
+ typer.Option(
158
+ "--trace-out",
159
+ help="Export Chromium / Perfetto JSON trace for https://ui.perfetto.dev",
160
+ ),
161
+ ] = None,
162
+ ) -> None:
163
+ """Run a single LLM serving simulation scenario."""
164
+ hw = get_hardware_profile(hardware)
165
+ p_mode = (
166
+ PreemptionMode.SWAP
167
+ if preemption.lower() == "swap"
168
+ else PreemptionMode.RECOMPUTE
169
+ )
170
+
171
+ sched_instance = create_scheduler(
172
+ scheduler,
173
+ hw,
174
+ p_mode,
175
+ token_budget=token_budget,
176
+ enable_prefix_caching=enable_prefix_cache,
177
+ )
178
+ simulator = Simulator(hw, sched_instance, preemption_mode=p_mode)
179
+
180
+ requests = generate_workload(
181
+ rate_req_per_s=rate,
182
+ duration_s=duration,
183
+ workload_type=workload,
184
+ pattern=pattern,
185
+ seed=seed,
186
+ )
187
+ simulator.submit_workload(requests)
188
+
189
+ with console.status(f"[bold cyan]Simulating {sched_instance.name} on {hw.name}..."):
190
+ metrics = simulator.run()
191
+
192
+ print_single_run_summary(metrics)
193
+
194
+ if gantt and simulator.iteration_records:
195
+ console.print()
196
+ print_gantt_timeline(simulator.iteration_records, max_steps=12)
197
+
198
+ if output:
199
+ if output.suffix.lower() == ".json":
200
+ export_metrics_json(metrics, output)
201
+ console.print(f"\n[green]Saved JSON report to {output}[/green]")
202
+ elif output.suffix.lower() == ".md":
203
+ export_metrics_markdown(metrics, output)
204
+ console.print(f"\n[green]Saved Markdown report to {output}[/green]")
205
+
206
+ if trace_out:
207
+ export_perfetto_trace(
208
+ metrics, simulator.iteration_records, simulator.all_requests, trace_out
209
+ )
210
+ console.print(
211
+ f"\n[green]Saved Perfetto trace to {trace_out} (load at https://ui.perfetto.dev)[/green]"
212
+ )
213
+
214
+
215
+ @app.command()
216
+ def compare(
217
+ schedulers: Annotated[
218
+ str,
219
+ typer.Option(
220
+ "--schedulers",
221
+ help="Comma-separated list: fcfs,sjf,chunked,slo,disaggregated",
222
+ ),
223
+ ] = "fcfs,sjf,chunked,slo,disaggregated",
224
+ workload: Annotated[
225
+ str,
226
+ typer.Option(
227
+ "--workload", "-w", help="Workload type: chatbot, rag, codegen, mixed"
228
+ ),
229
+ ] = "rag",
230
+ hardware: Annotated[
231
+ str,
232
+ typer.Option(
233
+ "--hardware",
234
+ "-h",
235
+ help="Hardware preset: a100, h100, t4, rtx3050, or path to custom JSON",
236
+ ),
237
+ ] = "a100",
238
+ rate: Annotated[
239
+ float, typer.Option("--rate", "-r", help="Arrival rate in requests/sec")
240
+ ] = 8.0,
241
+ duration: Annotated[
242
+ float, typer.Option("--duration", "-d", help="Simulation duration in seconds")
243
+ ] = 15.0,
244
+ token_budget: Annotated[
245
+ int,
246
+ typer.Option(
247
+ "--token-budget", help="Token budget per step for chunked prefill"
248
+ ),
249
+ ] = 512,
250
+ seed: Annotated[
251
+ int, typer.Option("--seed", help="Random seed for deterministic comparison")
252
+ ] = 42,
253
+ ) -> None:
254
+ """Run multiple scheduling policies side-by-side on the exact same workload trace."""
255
+ hw = get_hardware_profile(hardware)
256
+ scheduler_keys = [s.strip() for s in schedulers.split(",") if s.strip()]
257
+
258
+ # Generate the identical workload trace once
259
+ base_requests = generate_workload(
260
+ rate_req_per_s=rate,
261
+ duration_s=duration,
262
+ workload_type=workload,
263
+ seed=seed,
264
+ )
265
+
266
+ results = []
267
+ with console.status(
268
+ f"[bold magenta]Comparing {len(scheduler_keys)} schedulers on identical trace..."
269
+ ):
270
+ for key in scheduler_keys:
271
+ # Deep clone requests for fair isolation
272
+ req_clones = [r.model_copy(deep=True) for r in base_requests]
273
+ sched_instance = create_scheduler(key, hw, token_budget=token_budget)
274
+ sim = Simulator(hw, sched_instance)
275
+ sim.submit_workload(req_clones)
276
+ m = sim.run()
277
+ results.append(m)
278
+
279
+ print_comparison_table(results)
280
+
281
+
282
+ @app.command()
283
+ def trace(
284
+ trace_file: Annotated[Path, typer.Argument(help="Path to trace JSON file")],
285
+ scheduler: Annotated[
286
+ str, typer.Option("--scheduler", "-s", help="Scheduler policy")
287
+ ] = "chunked",
288
+ hardware: Annotated[
289
+ str,
290
+ typer.Option(
291
+ "--hardware",
292
+ "-h",
293
+ help="Hardware preset: a100, h100, t4, rtx3050, or path to custom JSON",
294
+ ),
295
+ ] = "a100",
296
+ token_budget: Annotated[
297
+ int, typer.Option("--token-budget", help="Chunked prefill budget")
298
+ ] = 512,
299
+ enable_prefix_cache: Annotated[
300
+ bool,
301
+ typer.Option(
302
+ "--prefix-cache/--no-prefix-cache",
303
+ help="Enable Radix prefix caching in KV-cache block manager",
304
+ ),
305
+ ] = False,
306
+ gantt: Annotated[
307
+ bool, typer.Option("--gantt/--no-gantt", help="Show Gantt timeline")
308
+ ] = True,
309
+ trace_out: Annotated[
310
+ Path | None,
311
+ typer.Option(
312
+ "--trace-out",
313
+ help="Export Chromium / Perfetto JSON trace for https://ui.perfetto.dev",
314
+ ),
315
+ ] = None,
316
+ ) -> None:
317
+ """Replay a real-world trace file through the simulator."""
318
+ hw = get_hardware_profile(hardware)
319
+ sched_instance = create_scheduler(
320
+ scheduler,
321
+ hw,
322
+ token_budget=token_budget,
323
+ enable_prefix_caching=enable_prefix_cache,
324
+ )
325
+ sim = Simulator(hw, sched_instance)
326
+
327
+ requests = load_trace_file(trace_file)
328
+ sim.submit_workload(requests)
329
+
330
+ with console.status(f"[bold cyan]Replaying trace ({len(requests)} requests)..."):
331
+ metrics = sim.run()
332
+
333
+ print_single_run_summary(metrics)
334
+ if gantt and sim.iteration_records:
335
+ console.print()
336
+ print_gantt_timeline(sim.iteration_records, max_steps=12)
337
+
338
+ if trace_out:
339
+ export_perfetto_trace(
340
+ metrics, sim.iteration_records, sim.all_requests, trace_out
341
+ )
342
+ console.print(
343
+ f"\n[green]Saved Perfetto trace to {trace_out} (load at https://ui.perfetto.dev)[/green]"
344
+ )
345
+
346
+
347
+ @app.command()
348
+ def calibrate(
349
+ servebench_result: Annotated[
350
+ Path,
351
+ typer.Option(
352
+ "--servebench-result", "-s", help="Path to servebench JSON export file"
353
+ ),
354
+ ],
355
+ output_preset: Annotated[
356
+ Path | None,
357
+ typer.Option("--output-preset", "-o", help="Optional output JSON preset file"),
358
+ ] = None,
359
+ ) -> None:
360
+ """Calibrate hardware simulation parameters directly from a servebench benchmark export."""
361
+ if not servebench_result.exists():
362
+ console.print(f"[red]Error: file {servebench_result} not found.[/red]")
363
+ raise typer.Exit(code=1)
364
+
365
+ with open(servebench_result, "r", encoding="utf-8") as f:
366
+ data = json.load(f)
367
+
368
+ # Read measured latency percentiles from servebench
369
+ measured_ttft = 150.0
370
+ measured_tpot = 35.0
371
+
372
+ if "levels" in data and len(data["levels"]) > 0:
373
+ lvl_summary = data["levels"][0].get("summary", {})
374
+ ttft_val = lvl_summary.get("ttft", {}).get("mean")
375
+ tpot_val = lvl_summary.get("tpot", {}).get("mean")
376
+ if ttft_val is not None:
377
+ measured_ttft = ttft_val * 1000.0 if ttft_val < 5.0 else ttft_val
378
+ if tpot_val is not None:
379
+ measured_tpot = tpot_val * 1000.0 if tpot_val < 5.0 else tpot_val
380
+ elif "summary" in data:
381
+ s = data["summary"]
382
+ ttft_val = s.get("ttft_ms", {}).get("mean") or s.get("ttft", {}).get("mean")
383
+ tpot_val = s.get("tpot_ms", {}).get("mean") or s.get("tpot", {}).get("mean")
384
+ if ttft_val is not None:
385
+ measured_ttft = ttft_val * 1000.0 if ttft_val < 5.0 else ttft_val
386
+ if tpot_val is not None:
387
+ measured_tpot = tpot_val * 1000.0 if tpot_val < 5.0 else tpot_val
388
+
389
+ console.print(
390
+ f"[bold green][OK] Loaded servebench benchmark data from {servebench_result.name}[/bold green]"
391
+ )
392
+ console.print(f" * Measured TTFT mean: {measured_ttft:.2f} ms")
393
+ console.print(f" * Measured TPOT mean: {measured_tpot:.2f} ms")
394
+
395
+ model_name = data.get("config", {}).get("model", "") or ""
396
+ base_hw = (
397
+ PRESETS["rtx3050_05b"] if "0.5b" in model_name.lower() else PRESETS["rtx3050"]
398
+ )
399
+
400
+ # Base raw weight memory streaming time (ms) at theoretical DRAM bandwidth
401
+ base_mem_ms = (
402
+ (base_hw.parameter_count_billions * 1e9 * base_hw.dtype_bytes)
403
+ / (base_hw.memory_bandwidth_gb_s * 1e9)
404
+ ) * 1000.0
405
+
406
+ if measured_tpot > base_mem_ms:
407
+ # Software dispatch overhead (e.g., Python runtime, eager kernel dispatch)
408
+ calibrated_launch = measured_tpot - base_mem_ms
409
+ calibrated_bw = base_hw.memory_bandwidth_gb_s
410
+ else:
411
+ # High MBU or quantization: calibrate effective weights memory streaming bandwidth
412
+ calibrated_launch = 0.02
413
+ calibrated_bw = (
414
+ base_hw.parameter_count_billions * 1e9 * base_hw.dtype_bytes
415
+ ) / (max(0.001, (measured_tpot - 0.02) / 1000.0) * 1e9)
416
+
417
+ calibrated_efficiency = max(
418
+ 0.30, min(0.75, (150.0 / max(40.0, measured_ttft)) * 0.48)
419
+ )
420
+
421
+ calibrated_hw = base_hw.model_copy(
422
+ update={
423
+ "name": f"Calibrated ({servebench_result.stem})",
424
+ "kernel_launch_overhead_ms": round(calibrated_launch, 3),
425
+ "memory_bandwidth_gb_s": round(calibrated_bw, 1),
426
+ "tensor_core_efficiency": round(calibrated_efficiency, 3),
427
+ }
428
+ )
429
+
430
+ console.print(
431
+ f" * Calibrated Launch Overhead: {calibrated_hw.kernel_launch_overhead_ms} ms"
432
+ )
433
+ console.print(
434
+ f" * Calibrated Memory Bandwidth: {calibrated_hw.memory_bandwidth_gb_s} GB/s"
435
+ )
436
+ console.print(
437
+ f" * Calibrated Tensor Core Efficiency: {calibrated_hw.tensor_core_efficiency * 100:.1f}%"
438
+ )
439
+
440
+ if output_preset:
441
+ output_preset.parent.mkdir(parents=True, exist_ok=True)
442
+ with open(output_preset, "w", encoding="utf-8") as f:
443
+ f.write(calibrated_hw.model_dump_json(indent=2))
444
+ console.print(f"[green]Saved calibrated preset to {output_preset}[/green]")
445
+
446
+
447
+ @app.command()
448
+ def sweep(
449
+ schedulers: Annotated[
450
+ str, typer.Option("--schedulers", help="Comma-separated schedulers to sweep")
451
+ ] = "fcfs,chunked,slo,disaggregated",
452
+ workload: Annotated[
453
+ str,
454
+ typer.Option(
455
+ "--workload", "-w", help="Workload type: chatbot, rag, codegen, mixed"
456
+ ),
457
+ ] = "rag",
458
+ hardware: Annotated[
459
+ str,
460
+ typer.Option(
461
+ "--hardware",
462
+ "-h",
463
+ help="Hardware preset: a100, h100, t4, rtx3050, or path to custom JSON",
464
+ ),
465
+ ] = "a100",
466
+ min_rate: Annotated[
467
+ float, typer.Option("--min-rate", help="Starting arrival rate (req/s)")
468
+ ] = 2.0,
469
+ max_rate: Annotated[
470
+ float, typer.Option("--max-rate", help="Maximum arrival rate (req/s)")
471
+ ] = 18.0,
472
+ step: Annotated[float, typer.Option("--step", help="Rate increment step")] = 4.0,
473
+ duration: Annotated[
474
+ float,
475
+ typer.Option("--duration", "-d", help="Simulation duration at each rate step"),
476
+ ] = 10.0,
477
+ token_budget: Annotated[
478
+ int, typer.Option("--token-budget", help="Chunked prefill budget")
479
+ ] = 512,
480
+ seed: Annotated[int, typer.Option("--seed", help="Random seed")] = 42,
481
+ output: Annotated[
482
+ Path | None, typer.Option("--output", "-o", help="Optional output JSON path")
483
+ ] = None,
484
+ ) -> None:
485
+ """Sweep arrival rate to discover scheduler saturation knees and Goodput cliffs."""
486
+ hw = get_hardware_profile(hardware)
487
+ scheduler_keys = [s.strip() for s in schedulers.split(",") if s.strip()]
488
+
489
+ rates: list[float] = []
490
+ curr = min_rate
491
+ while curr <= max_rate + 1e-5:
492
+ rates.append(round(curr, 2))
493
+ curr += step
494
+
495
+ console.print(
496
+ f"[bold cyan]Initiating load sweep across {len(rates)} rate steps ({min_rate} -> {max_rate} req/s)...[/bold cyan]"
497
+ )
498
+
499
+ sweep_records: list[dict] = []
500
+ for r in rates:
501
+ base_requests = generate_workload(
502
+ rate_req_per_s=r,
503
+ duration_s=duration,
504
+ workload_type=workload,
505
+ seed=seed,
506
+ )
507
+ rate_entry = {"rate": r, "results": {}}
508
+ with console.status(
509
+ f"[bold yellow]Simulating load: {r:.1f} req/s ({len(base_requests)} requests)..."
510
+ ):
511
+ for key in scheduler_keys:
512
+ req_clones = [req.model_copy(deep=True) for req in base_requests]
513
+ sched_instance = create_scheduler(key, hw, token_budget=token_budget)
514
+ sim = Simulator(hw, sched_instance)
515
+ sim.submit_workload(req_clones)
516
+ m = sim.run()
517
+ rate_entry["results"][sched_instance.name] = {
518
+ "goodput": m.goodput_ratio,
519
+ "ttft_p99": m.ttft_ms.p99,
520
+ "throughput_tok_s": m.throughput_tok_per_s,
521
+ }
522
+ sweep_records.append(rate_entry)
523
+
524
+ # Render Pareto Saturation Table
525
+ table = Table(
526
+ title=f"Saturation Pareto Curve - Goodput vs Load ({hw.name})",
527
+ show_header=True,
528
+ header_style="bold magenta",
529
+ )
530
+ table.add_column("Arrival Rate", style="bold cyan", width=14, justify="right")
531
+
532
+ # Get sample scheduler names
533
+ sample_names = list(sweep_records[0]["results"].keys())
534
+ for name in sample_names:
535
+ table.add_column(f"{name} (Goodput %)", justify="right")
536
+
537
+ for entry in sweep_records:
538
+ row = [f"{entry['rate']:.1f} req/s"]
539
+ for name in sample_names:
540
+ gp = entry["results"][name]["goodput"]
541
+ color = "green" if gp >= 90.0 else ("yellow" if gp >= 60.0 else "red")
542
+ row.append(f"[{color}]{gp:.1f}%[/{color}]")
543
+ table.add_row(*row)
544
+
545
+ console.print(table)
546
+
547
+ if output:
548
+ output.parent.mkdir(parents=True, exist_ok=True)
549
+ with open(output, "w", encoding="utf-8") as f:
550
+ json.dump(sweep_records, f, indent=2)
551
+ console.print(f"\n[green]Saved sweep data to {output}[/green]")
552
+
553
+
554
+ @app.command()
555
+ def info() -> None:
556
+ """Display available silicon hardware presets and workload distribution profiles."""
557
+ hw_table = Table(
558
+ title="Hardware Silicon Presets", show_header=True, header_style="bold cyan"
559
+ )
560
+ hw_table.add_column("Key", style="bold yellow")
561
+ hw_table.add_column("Silicon Target", style="white")
562
+ hw_table.add_column("VRAM", justify="right")
563
+ hw_table.add_column("DRAM Bandwidth", justify="right")
564
+ hw_table.add_column("Compute (FP16)", justify="right")
565
+ hw_table.add_column("Default Model", style="dim")
566
+
567
+ for k, v in PRESETS.items():
568
+ hw_table.add_row(
569
+ k,
570
+ v.name,
571
+ f"{v.vram_gb:.0f} GB",
572
+ f"{v.memory_bandwidth_gb_s:.0f} GB/s",
573
+ f"{v.fp16_tflops:.1f} TFLOPS",
574
+ v.model_name,
575
+ )
576
+ console.print(hw_table)
577
+
578
+ dist_table = Table(
579
+ title="Workload Distribution Profiles",
580
+ show_header=True,
581
+ header_style="bold magenta",
582
+ )
583
+ dist_table.add_column("Key", style="bold yellow")
584
+ dist_table.add_column("Domain", style="white")
585
+ dist_table.add_column("Prompt Length (Min-Mean-Max)", justify="center")
586
+ dist_table.add_column("Output Length (Min-Mean-Max)", justify="center")
587
+ dist_table.add_column("Target TTFT SLO", justify="right")
588
+
589
+ for k, v in DISTRIBUTIONS.items():
590
+ dist_table.add_row(
591
+ k,
592
+ v.name,
593
+ f"{v.prompt_min} - {v.prompt_mean} - {v.prompt_max}t",
594
+ f"{v.output_min} - {v.output_mean} - {v.output_max}t",
595
+ f"{v.slo_ttft_s * 1000:.0f} ms",
596
+ )
597
+ console.print(dist_table)
598
+
599
+
600
+ if __name__ == "__main__":
601
+ app()