most-queue 2.8__py3-none-any.whl → 2.11__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.
- most_queue/__init__.py +7 -1
- most_queue/io/serving.py +258 -0
- most_queue/random/feature_service.py +175 -0
- most_queue/random/map_fit.py +137 -0
- most_queue/random/map_ph.py +48 -0
- most_queue/random/marked_arrivals.py +127 -0
- most_queue/random/queue_selection.py +47 -0
- most_queue/random/service_calibration.py +91 -0
- most_queue/random/trace_resampling.py +145 -0
- most_queue/random/utils/fit.py +20 -7
- most_queue/sim/accumulating_priority.py +118 -0
- most_queue/sim/admission_control.py +90 -0
- most_queue/sim/aoi.py +133 -0
- most_queue/sim/base.py +4 -2
- most_queue/sim/base_core.py +40 -3
- most_queue/sim/bmap.py +130 -0
- most_queue/sim/bulk_map_finite.py +191 -0
- most_queue/sim/bulk_service.py +95 -0
- most_queue/sim/decaying_value.py +188 -0
- most_queue/sim/delay_dependent.py +115 -0
- most_queue/sim/dynamic_batching.py +236 -0
- most_queue/sim/edf.py +162 -0
- most_queue/sim/heterogeneous_bulk_service.py +237 -0
- most_queue/sim/imprecise.py +258 -0
- most_queue/sim/inventory.py +687 -0
- most_queue/sim/kv_cache_congestion.py +226 -0
- most_queue/sim/kv_cache_stability.py +219 -0
- most_queue/sim/load_balancing.py +98 -0
- most_queue/sim/msj.py +117 -0
- most_queue/sim/msj_checkpoint.py +273 -0
- most_queue/sim/msj_general.py +535 -0
- most_queue/sim/msj_lifecycle.py +355 -0
- most_queue/sim/negative.py +13 -16
- most_queue/sim/networks/closed_network.py +147 -0
- most_queue/sim/networks/fork_join_network.py +167 -0
- most_queue/sim/networks/network.py +12 -2
- most_queue/sim/networks/tandem_blocking.py +142 -0
- most_queue/sim/networks/time_varying_network.py +124 -0
- most_queue/sim/polling.py +87 -0
- most_queue/sim/prefill_decode.py +234 -0
- most_queue/sim/priority.py +35 -0
- most_queue/sim/priority_heterogeneous.py +89 -0
- most_queue/sim/priority_impatience.py +105 -0
- most_queue/sim/reliability.py +413 -0
- most_queue/sim/retrial_priority.py +104 -0
- most_queue/sim/smdp_batching.py +170 -0
- most_queue/sim/time_varying.py +69 -0
- most_queue/sim/token_batching.py +181 -0
- most_queue/sim/utils/acme_trace.py +204 -0
- most_queue/sim/utils/llm_trace.py +101 -0
- most_queue/sim/utils/msj_calendar.py +66 -0
- most_queue/sim/utils/msj_capacity_calendar.py +104 -0
- most_queue/sim/utils/msj_packing.py +98 -0
- most_queue/sim/utils/residual_runtime.py +145 -0
- most_queue/sim/utils/resource_envelope.py +135 -0
- most_queue/sim/utils/runtime_prediction.py +239 -0
- most_queue/sim/utils/workload_lifecycle.py +101 -0
- most_queue/sim/utils/workload_trace.py +172 -0
- most_queue/sim/value_scheduling.py +439 -0
- most_queue/structs.py +465 -1
- most_queue/theory/__init__.py +2 -0
- most_queue/theory/admission_control/__init__.py +14 -0
- most_queue/theory/admission_control/mm1_deadline_admission.py +204 -0
- most_queue/theory/aoi/__init__.py +5 -0
- most_queue/theory/aoi/calc.py +114 -0
- most_queue/theory/batch/__init__.py +9 -0
- most_queue/theory/batch/_idle_refill.py +240 -0
- most_queue/theory/batch/_tagged_batch.py +203 -0
- most_queue/theory/batch/_tagged_boundary.py +76 -0
- most_queue/theory/batch/bulk_service.py +381 -0
- most_queue/theory/batch/bulk_service_erlang.py +401 -0
- most_queue/theory/batch/bulk_service_general.py +79 -0
- most_queue/theory/batch/bulk_service_h2.py +546 -0
- most_queue/theory/batch/bulk_service_multiserver.py +697 -0
- most_queue/theory/batch/callback_batching.py +125 -0
- most_queue/theory/batch/dynamic_batching.py +396 -0
- most_queue/theory/batch/heterogeneous_bulk_service.py +369 -0
- most_queue/theory/batch/map_ph_finite_buffer.py +695 -0
- most_queue/theory/batch/mm1.py +3 -2
- most_queue/theory/batch/smdp_batching.py +252 -0
- most_queue/theory/batch/token_batching.py +321 -0
- most_queue/theory/batch/token_service.py +276 -0
- most_queue/theory/closed/__init__.py +7 -0
- most_queue/theory/continuous_batching/__init__.py +42 -0
- most_queue/theory/continuous_batching/core_aware_ttft.py +218 -0
- most_queue/theory/continuous_batching/infinite_stage_aware_ttft.py +255 -0
- most_queue/theory/continuous_batching/kv_cache_congestion.py +241 -0
- most_queue/theory/continuous_batching/kv_cache_stability.py +151 -0
- most_queue/theory/continuous_batching/occupancy_dependent.py +183 -0
- most_queue/theory/continuous_batching/occupancy_dependent_h2.py +272 -0
- most_queue/theory/continuous_batching/phase_aware_stability.py +261 -0
- most_queue/theory/continuous_batching/phase_aware_ttft.py +157 -0
- most_queue/theory/continuous_batching/prefill_decode.py +211 -0
- most_queue/theory/continuous_batching/reduced_stage_aware_ttft.py +182 -0
- most_queue/theory/continuous_batching/serving_calibration.py +147 -0
- most_queue/theory/continuous_batching/stage_aware_ttft.py +245 -0
- most_queue/theory/continuous_batching/transient_occupancy.py +204 -0
- most_queue/theory/decaying_value/__init__.py +22 -0
- most_queue/theory/decaying_value/service_rate_control.py +419 -0
- most_queue/theory/delay_dependent/__init__.py +13 -0
- most_queue/theory/delay_dependent/mmc_threshold_service.py +494 -0
- most_queue/theory/fifo/__init__.py +14 -0
- most_queue/theory/fifo/_ps_sojourn.py +244 -0
- most_queue/theory/fifo/gi_m2_state_dependent.py +307 -0
- most_queue/theory/fifo/gi_m_1.py +10 -13
- most_queue/theory/fifo/mg1_ps.py +188 -7
- most_queue/theory/fifo/mgn_ps.py +140 -0
- most_queue/theory/fifo/mgn_takahasi.py +34 -1
- most_queue/theory/fifo/mm1_threshold_rate.py +299 -0
- most_queue/theory/fork_join/__init__.py +13 -0
- most_queue/theory/fork_join/dag.py +135 -0
- most_queue/theory/fork_join/split_join.py +46 -9
- most_queue/theory/impatience/__init__.py +11 -0
- most_queue/theory/imprecise/__init__.py +26 -0
- most_queue/theory/imprecise/offline.py +345 -0
- most_queue/theory/inventory/__init__.py +26 -0
- most_queue/theory/inventory/_wait_phase.py +431 -0
- most_queue/theory/inventory/mm1_inventory.py +285 -0
- most_queue/theory/inventory/mm1_inventory_erlang_replenishment.py +272 -0
- most_queue/theory/inventory/mm2_heterogeneous_inventory.py +294 -0
- most_queue/theory/inventory/mmc_heterogeneous_erlang_inventory.py +399 -0
- most_queue/theory/inventory/mmc_heterogeneous_h2_inventory.py +394 -0
- most_queue/theory/inventory/mmc_heterogeneous_inventory.py +348 -0
- most_queue/theory/inventory/mmc_inventory.py +351 -0
- most_queue/theory/load_balancing.py +99 -0
- most_queue/theory/matrix/__init__.py +16 -1
- most_queue/theory/matrix/bmap_m1.py +163 -0
- most_queue/theory/matrix/bmap_ph1.py +180 -0
- most_queue/theory/matrix/map_mmc.py +190 -0
- most_queue/theory/matrix/map_phc.py +278 -0
- most_queue/theory/msj/__init__.py +8 -0
- most_queue/theory/msj/exact.py +123 -0
- most_queue/theory/msj/ph.py +285 -0
- most_queue/theory/msj/saturated.py +138 -0
- most_queue/theory/negative/__init__.py +15 -0
- most_queue/theory/negative/mgn_disaster.py +37 -58
- most_queue/theory/negative/mgn_rcs.py +36 -202
- most_queue/theory/networks/__init__.py +28 -2
- most_queue/theory/networks/bcmp_network.py +283 -0
- most_queue/theory/networks/blocking.py +153 -0
- most_queue/theory/networks/closed_network.py +270 -0
- most_queue/theory/networks/fork_join_network.py +115 -0
- most_queue/theory/networks/g_network.py +259 -0
- most_queue/theory/networks/jackson_network.py +116 -0
- most_queue/theory/networks/qna.py +185 -0
- most_queue/theory/networks/time_varying_network.py +94 -0
- most_queue/theory/networks/traffic.py +23 -0
- most_queue/theory/polling.py +114 -0
- most_queue/theory/priority/__init__.py +29 -2
- most_queue/theory/priority/accumulating.py +108 -0
- most_queue/theory/priority/impatience.py +140 -0
- most_queue/theory/priority/map_ph_priority.py +235 -0
- most_queue/theory/priority/preemptive/mg1_repeat.py +155 -0
- most_queue/theory/priority/preemptive/mm2_3cls_busy_approx.py +9 -12
- most_queue/theory/priority/preemptive/mm2_heterogeneous.py +240 -0
- most_queue/theory/priority/preemptive/mmk_prty_exact.py +315 -0
- most_queue/theory/priority/preemptive/mmn_2cls_pr_busy_approx.py +9 -12
- most_queue/theory/priority/preemptive/mph_ph_k_2class.py +227 -0
- most_queue/theory/priority/preemptive/rdr_a.py +185 -0
- most_queue/theory/priority/retrial_priority.py +132 -0
- most_queue/theory/reliability/__init__.py +21 -0
- most_queue/theory/reliability/machine_repair.py +104 -0
- most_queue/theory/reliability/machine_repair_heterogeneous.py +150 -0
- most_queue/theory/reliability/mm1_disaster_repair.py +102 -0
- most_queue/theory/reliability/mm1_working_breakdowns.py +108 -0
- most_queue/theory/reliability/mmc_breakdowns.py +119 -0
- most_queue/theory/reliability/retrial_unreliable.py +110 -0
- most_queue/theory/reliability/utils.py +47 -0
- most_queue/theory/srpt/__init__.py +3 -0
- most_queue/theory/srpt/degradation.py +71 -0
- most_queue/theory/srpt/utils/load_below.py +13 -34
- most_queue/theory/time_varying.py +123 -0
- most_queue/theory/utils/__init__.py +12 -0
- most_queue/theory/utils/max_dist.py +236 -0
- most_queue/theory/utils/passage_time.py +48 -37
- most_queue/theory/utils/restarts.py +90 -14
- most_queue/theory/utils/sla.py +146 -0
- most_queue/theory/utils/tail.py +46 -0
- most_queue/theory/vacations/__init__.py +20 -0
- most_queue/theory/value_scheduling/__init__.py +25 -0
- most_queue/theory/value_scheduling/offline.py +336 -0
- most_queue-2.11.dist-info/METADATA +635 -0
- most_queue-2.11.dist-info/RECORD +301 -0
- {most_queue-2.8.dist-info → most_queue-2.11.dist-info}/WHEEL +1 -1
- most_queue-2.8.dist-info/METADATA +0 -185
- most_queue-2.8.dist-info/RECORD +0 -154
- {most_queue-2.8.dist-info → most_queue-2.11.dist-info}/licenses/LICENSE +0 -0
most_queue/__init__.py
CHANGED
|
@@ -5,12 +5,18 @@ This package provides both simulation and numerical calculation methods
|
|
|
5
5
|
for various types of queueing systems and networks.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
9
|
|
|
10
10
|
# Import main simulation and theory classes for easy access
|
|
11
11
|
from most_queue.sim.base import QsSim
|
|
12
12
|
from most_queue.theory.base_queue import BaseQueue
|
|
13
13
|
|
|
14
|
+
try:
|
|
15
|
+
# single source of truth for the version: the installed package metadata
|
|
16
|
+
__version__ = version("most-queue")
|
|
17
|
+
except PackageNotFoundError: # running from a source checkout that is not installed
|
|
18
|
+
__version__ = "0.0.0.dev0"
|
|
19
|
+
|
|
14
20
|
__all__ = [
|
|
15
21
|
"QsSim",
|
|
16
22
|
"BaseQueue",
|
most_queue/io/serving.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Audited request-level serving measurements and joint TTFT/TPOT SLOs.
|
|
2
|
+
|
|
3
|
+
Reference serializer: vLLM v0.33.0rc3, commit
|
|
4
|
+
4ba787ec9959e566d3b587a796dff07673324da5, vllm/benchmarks/serve.py.
|
|
5
|
+
TTFT and latency are client measurements; queue_times is CLIENT semaphore
|
|
6
|
+
waiting. Server timings are separate optional measurements, never inferred.
|
|
7
|
+
The SGLang array reader explicitly uses the span of streamed output events
|
|
8
|
+
when full per-request latencies are absent. See docs/serving_measurements.md.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ServingRequest:
|
|
21
|
+
"""One offered request; all durations are seconds and counts are actual.
|
|
22
|
+
|
|
23
|
+
A pending request has an unknown final latency/TPOT. Failed and cancelled
|
|
24
|
+
requests remain in the offered denominator. An absent measurement is None.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
input_tokens: int
|
|
28
|
+
output_tokens: int
|
|
29
|
+
status: str
|
|
30
|
+
ttft: float | None = None
|
|
31
|
+
latency: float | None = None
|
|
32
|
+
client_queue_time: float | None = None
|
|
33
|
+
server_queue_time: float | None = None
|
|
34
|
+
server_ttft: float | None = None
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
if self.status not in {"completed", "failed", "cancelled", "pending"}:
|
|
38
|
+
raise ValueError("unknown request status")
|
|
39
|
+
for count in (self.input_tokens, self.output_tokens):
|
|
40
|
+
if isinstance(count, bool) or not isinstance(count, (int, np.integer)) or count < 0:
|
|
41
|
+
raise ValueError("token counts must be nonnegative integers")
|
|
42
|
+
for value in (self.ttft, self.latency, self.client_queue_time, self.server_queue_time, self.server_ttft):
|
|
43
|
+
if value is not None and (not math.isfinite(value) or value < 0):
|
|
44
|
+
raise ValueError("measured durations must be finite and nonnegative, or None")
|
|
45
|
+
if self.status == "completed":
|
|
46
|
+
if min(self.input_tokens, self.output_tokens) < 1 or self.ttft is None or self.latency is None:
|
|
47
|
+
raise ValueError("completed text requests require positive lengths, TTFT and latency")
|
|
48
|
+
if self.latency < self.ttft:
|
|
49
|
+
raise ValueError("latency cannot precede the first token")
|
|
50
|
+
elif self.latency is not None:
|
|
51
|
+
raise ValueError("only completed requests have a final latency")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def tpot(self):
|
|
55
|
+
"""Per-request TPOT, excluding the first token; None for <=1 token."""
|
|
56
|
+
if self.status != "completed" or self.output_tokens <= 1:
|
|
57
|
+
return None
|
|
58
|
+
return (self.latency - self.ttft) / (self.output_tokens - 1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ServingBenchmark:
|
|
63
|
+
"""A finite offered cohort and its explicit measurement-window duration."""
|
|
64
|
+
|
|
65
|
+
requests: tuple[ServingRequest, ...]
|
|
66
|
+
duration: float
|
|
67
|
+
metadata: dict = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def __post_init__(self):
|
|
70
|
+
if not self.requests or not all(isinstance(row, ServingRequest) for row in self.requests):
|
|
71
|
+
raise ValueError("a benchmark requires a nonempty request cohort")
|
|
72
|
+
if not math.isfinite(self.duration) or self.duration <= 0:
|
|
73
|
+
raise ValueError("duration must be finite and positive")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_arrays(source, schema):
|
|
77
|
+
data = json.loads(Path(source).read_text(encoding="utf-8")) if isinstance(source, (str, Path)) else source
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
raise ValueError("expected a single benchmark JSON object")
|
|
80
|
+
required = ["input_lens", "output_lens", "ttfts", "errors"]
|
|
81
|
+
required += ["latencies"] if schema == "vllm-bench-serve" else ["itls"]
|
|
82
|
+
if any(not isinstance(data.get(key), list) for key in required):
|
|
83
|
+
raise ValueError("detailed per-request arrays are required; aggregate percentiles are insufficient")
|
|
84
|
+
count = len(data["input_lens"])
|
|
85
|
+
if not count or any(len(data[key]) != count for key in required):
|
|
86
|
+
raise ValueError("per-request arrays must be nonempty and aligned")
|
|
87
|
+
optional = ["queue_times", "server_queue_times", "server_ttfts"]
|
|
88
|
+
for key in optional:
|
|
89
|
+
if key in data and (not isinstance(data[key], list) or len(data[key]) != count):
|
|
90
|
+
raise ValueError(f"{key} must align with the request arrays")
|
|
91
|
+
rows = []
|
|
92
|
+
for index in range(count):
|
|
93
|
+
error = data["errors"][index]
|
|
94
|
+
if not isinstance(error, str):
|
|
95
|
+
raise ValueError("each error must be a string")
|
|
96
|
+
completed = error == "" and data["output_lens"][index] > 0
|
|
97
|
+
latency = None
|
|
98
|
+
if completed:
|
|
99
|
+
if schema == "vllm-bench-serve":
|
|
100
|
+
latency = data["latencies"][index]
|
|
101
|
+
else:
|
|
102
|
+
gaps = np.asarray(data["itls"][index], dtype=float)
|
|
103
|
+
if gaps.ndim != 1 or not np.all(np.isfinite(gaps)) or np.any(gaps < 0):
|
|
104
|
+
raise ValueError("stream gaps must be finite nonnegative vectors")
|
|
105
|
+
latency = float(data["ttfts"][index] + gaps.sum())
|
|
106
|
+
timings = {key: data[key][index] if completed and key in data else None for key in optional}
|
|
107
|
+
rows.append(
|
|
108
|
+
ServingRequest(
|
|
109
|
+
input_tokens=data["input_lens"][index],
|
|
110
|
+
output_tokens=data["output_lens"][index],
|
|
111
|
+
status="completed" if completed else "failed",
|
|
112
|
+
ttft=data["ttfts"][index] if completed else None,
|
|
113
|
+
latency=latency,
|
|
114
|
+
client_queue_time=timings["queue_times"],
|
|
115
|
+
server_queue_time=timings["server_queue_times"],
|
|
116
|
+
server_ttft=timings["server_ttfts"],
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
successes = sum(row.status == "completed" for row in rows)
|
|
120
|
+
if data.get("completed") != successes or ("failed" in data and data["failed"] != count - successes):
|
|
121
|
+
raise ValueError("aggregate completion/failure counts disagree with detailed outcomes")
|
|
122
|
+
if "num_prompts" in data and data["num_prompts"] != count:
|
|
123
|
+
raise ValueError("num_prompts disagrees with offered cohort; do not silently drop requests")
|
|
124
|
+
metadata = {
|
|
125
|
+
key: data[key]
|
|
126
|
+
for key in ["model_id", "backend", "max_concurrency", "request_rate", "dataset_name"]
|
|
127
|
+
if key in data
|
|
128
|
+
}
|
|
129
|
+
metadata.update(schema=schema, latency_basis="client_latency" if schema == "vllm-bench-serve" else "stream_span")
|
|
130
|
+
if metadata.get("request_rate") == math.inf:
|
|
131
|
+
metadata["request_rate"] = "inf"
|
|
132
|
+
return ServingBenchmark(tuple(rows), data["duration"], metadata)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def read_vllm_benchmark(source, *, measurement_profile=None):
|
|
136
|
+
"""Read vLLM detailed JSON; preserve absent server metrics as None.
|
|
137
|
+
|
|
138
|
+
The caller must retain the producer version and hardware provenance. This
|
|
139
|
+
is a schema adapter, not detection of the engine/version from latency data.
|
|
140
|
+
"""
|
|
141
|
+
result = _read_arrays(source, "vllm-bench-serve")
|
|
142
|
+
result.metadata["measurement_profile"] = measurement_profile
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def read_sglang_stream_benchmark(source, *, measurement_profile=None):
|
|
147
|
+
"""Read detailed streaming arrays with TTFT + sum(ITL) as observed span.
|
|
148
|
+
|
|
149
|
+
The span ends at the last output event, not necessarily HTTP completion.
|
|
150
|
+
ITL entries can contain multiple tokens; TPOT uses actual output_tokens.
|
|
151
|
+
"""
|
|
152
|
+
result = _read_arrays(source, "sglang-stream-arrays")
|
|
153
|
+
result.metadata["measurement_profile"] = measurement_profile
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _quantiles(values):
|
|
158
|
+
if not values:
|
|
159
|
+
return None
|
|
160
|
+
return {
|
|
161
|
+
"count": len(values),
|
|
162
|
+
"mean": float(np.mean(values)),
|
|
163
|
+
**dict(zip(["p50", "p90", "p99"], np.percentile(values, [50, 90, 99]).tolist())),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def summarize_serving_slo(benchmark, *, ttft_limit, tpot_limit):
|
|
168
|
+
"""Joint request SLO with all offered outcomes and pending-outcome bounds.
|
|
169
|
+
|
|
170
|
+
Completed one-token replies pass the TPOT condition by the vLLM goodput
|
|
171
|
+
convention but are excluded from TPOT means/quantiles. Pending requests
|
|
172
|
+
are unknown unless their already-observed TTFT violates its limit.
|
|
173
|
+
Counts/quantiles describe this cohort, without an iid confidence claim.
|
|
174
|
+
"""
|
|
175
|
+
if any(not math.isfinite(value) or value < 0 for value in (ttft_limit, tpot_limit)):
|
|
176
|
+
raise ValueError("SLO limits must be finite nonnegative seconds")
|
|
177
|
+
counts = {status: 0 for status in ["completed", "failed", "cancelled", "pending"]}
|
|
178
|
+
passed, unknown, single_token = 0, 0, 0
|
|
179
|
+
ttfts, tpots = [], []
|
|
180
|
+
for row in benchmark.requests:
|
|
181
|
+
counts[row.status] += 1
|
|
182
|
+
if row.status == "completed":
|
|
183
|
+
ttfts.append(row.ttft)
|
|
184
|
+
if row.tpot is None:
|
|
185
|
+
single_token += 1
|
|
186
|
+
else:
|
|
187
|
+
tpots.append(row.tpot)
|
|
188
|
+
passed += row.ttft <= ttft_limit and (row.tpot is None or row.tpot <= tpot_limit)
|
|
189
|
+
elif row.status == "pending" and (row.ttft is None or row.ttft <= ttft_limit):
|
|
190
|
+
unknown += 1
|
|
191
|
+
offered = len(benchmark.requests)
|
|
192
|
+
return {
|
|
193
|
+
"offered": offered,
|
|
194
|
+
**counts,
|
|
195
|
+
"single_token_completed": single_token,
|
|
196
|
+
"joint_passed": passed,
|
|
197
|
+
"joint_unknown": unknown,
|
|
198
|
+
"joint_fraction_bounds": [passed / offered, (passed + unknown) / offered],
|
|
199
|
+
"observed_goodput": passed / benchmark.duration,
|
|
200
|
+
"duration": benchmark.duration,
|
|
201
|
+
"ttft_limit": ttft_limit,
|
|
202
|
+
"tpot_limit": tpot_limit,
|
|
203
|
+
"completed_ttft_seconds": _quantiles(ttfts),
|
|
204
|
+
"completed_multitoken_tpot_seconds": _quantiles(tpots),
|
|
205
|
+
"server_queue_observed": sum(row.server_queue_time is not None for row in benchmark.requests),
|
|
206
|
+
"server_ttft_observed": sum(row.server_ttft is not None for row in benchmark.requests),
|
|
207
|
+
"latency_basis": benchmark.metadata.get("latency_basis", "explicit_request_records"),
|
|
208
|
+
"uncertainty": "pending-outcome bounds; no iid confidence interval",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def from_prefill_decode(result, arrivals, inputs, outputs, *, horizon=None):
|
|
213
|
+
"""Convert token DES timestamps; optionally censor at a fixed horizon.
|
|
214
|
+
|
|
215
|
+
Future requests are outside the offered cohort. Future completions and
|
|
216
|
+
tokens are hidden rather than counted as successful outcomes at the cut.
|
|
217
|
+
No steady-state or stationarity assertion is made for the drained cohort.
|
|
218
|
+
"""
|
|
219
|
+
arrivals = np.asarray(arrivals, dtype=float)
|
|
220
|
+
if arrivals.ndim != 1 or not arrivals.size or not np.all(np.isfinite(arrivals)) or np.any(np.diff(arrivals) < 0):
|
|
221
|
+
raise ValueError("arrivals must be nonempty finite chronological times")
|
|
222
|
+
if any(len(values) != len(arrivals) for values in [inputs, outputs, result.output_times, result.completion_times]):
|
|
223
|
+
raise ValueError("DES arrays must align")
|
|
224
|
+
for lengths in (inputs, outputs):
|
|
225
|
+
values = np.asarray(lengths, dtype=float)
|
|
226
|
+
if not np.all(np.isfinite(values)) or np.any(values <= 0) or np.any(values != np.floor(values)):
|
|
227
|
+
raise ValueError("DES token lengths must be positive integers")
|
|
228
|
+
for arrival, count, stamps, completion in zip(arrivals, outputs, result.output_times, result.completion_times):
|
|
229
|
+
stamps = np.asarray(stamps, dtype=float)
|
|
230
|
+
if stamps.ndim != 1 or stamps.size != count or not np.all(np.isfinite(stamps)):
|
|
231
|
+
raise ValueError("DES token timestamps must align with output lengths and be finite")
|
|
232
|
+
if (
|
|
233
|
+
np.any(np.diff(stamps) < 0)
|
|
234
|
+
or stamps[0] < arrival
|
|
235
|
+
or not math.isfinite(completion)
|
|
236
|
+
or stamps[-1] != completion
|
|
237
|
+
):
|
|
238
|
+
raise ValueError("DES token timestamps, output lengths and completion must agree")
|
|
239
|
+
end = max(result.completion_times) if horizon is None else horizon
|
|
240
|
+
if not math.isfinite(end) or end <= arrivals[0]:
|
|
241
|
+
raise ValueError("observation horizon must follow the first arrival")
|
|
242
|
+
rows = []
|
|
243
|
+
for index, arrival in enumerate(arrivals):
|
|
244
|
+
if arrival > end:
|
|
245
|
+
break
|
|
246
|
+
tokens = result.output_times[index]
|
|
247
|
+
observed = [stamp for stamp in tokens if stamp <= end]
|
|
248
|
+
completed = result.completion_times[index] <= end
|
|
249
|
+
rows.append(
|
|
250
|
+
ServingRequest(
|
|
251
|
+
input_tokens=int(inputs[index]),
|
|
252
|
+
output_tokens=int(outputs[index]) if completed else len(observed),
|
|
253
|
+
status="completed" if completed else "pending",
|
|
254
|
+
ttft=tokens[0] - arrival if observed else None,
|
|
255
|
+
latency=result.completion_times[index] - arrival if completed else None,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
return ServingBenchmark(tuple(rows), float(end - arrivals[0]), {"latency_basis": "token_DES"})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""History-only conditional ECDFs with audited fallback and optional scale ratios.
|
|
2
|
+
|
|
3
|
+
CRPS uses the empirical energy identity, Gneiting & Raftery (2007),
|
|
4
|
+
https://doi.org/10.1198/016214506000001437. It scores a full predictive CDF,
|
|
5
|
+
not a random sample from it. Feature availability must be established by callers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from numbers import Integral, Real
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from most_queue.random.service_calibration import ServiceCalibration
|
|
15
|
+
from most_queue.random.trace_resampling import ConditionalEmpirical
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _positive(value, name):
|
|
19
|
+
if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real) or not np.isfinite(value) or value <= 0:
|
|
20
|
+
raise ValueError(f"{name} must be positive and finite")
|
|
21
|
+
return float(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _need(value):
|
|
25
|
+
if isinstance(value, (bool, np.bool_)) or not isinstance(value, Integral) or value < 1:
|
|
26
|
+
raise ValueError("need must be a positive integer")
|
|
27
|
+
return int(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _context(value):
|
|
31
|
+
if value is not None and (not isinstance(value, str) or not value):
|
|
32
|
+
raise ValueError("context must be a nonempty string or None")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def request_bucket(request):
|
|
37
|
+
"""Fixed right-closed request bins in seconds; unknown is distinct from zero."""
|
|
38
|
+
if request is None:
|
|
39
|
+
return None
|
|
40
|
+
value = _positive(request, "request")
|
|
41
|
+
return str(int(np.searchsorted([300, 1800, 7200, 28800, 86400], value)))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class EmpiricalPrediction:
|
|
46
|
+
"""An immutable selected ECDF, positive target scale and audited fit level."""
|
|
47
|
+
|
|
48
|
+
distribution: ServiceCalibration
|
|
49
|
+
scale: float = 1.0
|
|
50
|
+
level: str = "pooled"
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
_positive(self.scale, "scale")
|
|
54
|
+
# Check the support before sampling, not only the selected quantile.
|
|
55
|
+
_positive(self.scale * self.distribution.samples[-1], "scaled support")
|
|
56
|
+
_positive(self.scale * self.distribution.samples[0], "scaled support")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def mean(self):
|
|
60
|
+
"""Predictive mean in service-time units, not ratio units."""
|
|
61
|
+
return self.scale * self.distribution.mean
|
|
62
|
+
|
|
63
|
+
def quantile(self, probability):
|
|
64
|
+
"""Inverse discrete ECDF; no interpolation or truncation at runtime budget."""
|
|
65
|
+
if isinstance(probability, (bool, np.bool_)) or not isinstance(probability, Real):
|
|
66
|
+
raise ValueError("probability must be a finite real in (0,1)")
|
|
67
|
+
return float(self.scale * self.distribution.quantiles("empirical", [probability])[0])
|
|
68
|
+
|
|
69
|
+
def probability_exceeding(self, threshold):
|
|
70
|
+
"""Return P(S > threshold), with equality counted as within the budget."""
|
|
71
|
+
threshold = _positive(threshold, "threshold")
|
|
72
|
+
support = np.array(self.distribution.samples) * self.scale
|
|
73
|
+
return float(np.mean(support > threshold))
|
|
74
|
+
|
|
75
|
+
def crps(self, observed):
|
|
76
|
+
"""Compute E|X-y| - .5 E|X-X'| exactly for a finite equal-weight ECDF.
|
|
77
|
+
|
|
78
|
+
For sorted support x_i the second term is sum((2i-n-1)x_i)/n².
|
|
79
|
+
Scaling before evaluation keeps score units in seconds for ratio fits.
|
|
80
|
+
"""
|
|
81
|
+
observed = _positive(observed, "observed")
|
|
82
|
+
support = np.asarray(self.distribution.samples) * self.scale
|
|
83
|
+
n = len(support)
|
|
84
|
+
dispersion = np.dot((2 * np.arange(1, n + 1) - n - 1) / n, support) / n
|
|
85
|
+
score = float(np.mean(np.abs(support - observed)) - dispersion)
|
|
86
|
+
if not np.isfinite(score):
|
|
87
|
+
raise ValueError("CRPS overflow; rescale observations")
|
|
88
|
+
return max(0.0, score)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class FeatureConditionalEmpirical:
|
|
93
|
+
"""Joint context/need cells, then coarse/pooled fallback; fit uses history only.
|
|
94
|
+
|
|
95
|
+
Optional scales fit service/scale on known positive scales, multiplying by
|
|
96
|
+
the supplied target scale at prediction. Unknown target scale or absent
|
|
97
|
+
ratio history falls back to absolute service. No outcome or test S is used.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
absolute: ConditionalEmpirical
|
|
101
|
+
pooled: ServiceCalibration | None
|
|
102
|
+
coarse: dict
|
|
103
|
+
joint: dict
|
|
104
|
+
exact_joint: dict
|
|
105
|
+
ratio: bool
|
|
106
|
+
audit: dict
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def fit(
|
|
110
|
+
cls, needs, services, contexts, *, scales=None, exact=False, minimum=20
|
|
111
|
+
): # pylint: disable=too-many-arguments
|
|
112
|
+
"""Fit context ECDFs with a fixed minimum count and explicit missing scales."""
|
|
113
|
+
needs, contexts = tuple(needs), tuple(contexts)
|
|
114
|
+
if len(contexts) != len(needs):
|
|
115
|
+
raise ValueError("contexts must match needs")
|
|
116
|
+
needs = tuple(_need(value) for value in needs)
|
|
117
|
+
contexts = tuple(_context(value) for value in contexts)
|
|
118
|
+
absolute = ConditionalEmpirical.fit(needs, services, minimum=minimum)
|
|
119
|
+
if not isinstance(exact, bool):
|
|
120
|
+
raise ValueError("exact must be boolean")
|
|
121
|
+
values = np.asarray(services, dtype=float)
|
|
122
|
+
ratio = scales is not None
|
|
123
|
+
scales = tuple(scales) if ratio else (1.0,) * len(needs)
|
|
124
|
+
if len(scales) != len(needs):
|
|
125
|
+
raise ValueError("scales must match needs")
|
|
126
|
+
pools = {"coarse": defaultdict(list), "joint": defaultdict(list), "exact": defaultdict(list)}
|
|
127
|
+
normalized = []
|
|
128
|
+
for need, service, context, scale in zip(needs, values, contexts, scales):
|
|
129
|
+
if scale is None:
|
|
130
|
+
continue
|
|
131
|
+
value = _positive(service / _positive(scale, "scale"), "scaled service")
|
|
132
|
+
group = int(np.searchsorted([1, 8, 32], need))
|
|
133
|
+
normalized.append(value)
|
|
134
|
+
pools["coarse"][group].append(value)
|
|
135
|
+
pools["joint"][group, context].append(value)
|
|
136
|
+
if exact:
|
|
137
|
+
pools["exact"][need, context].append(value)
|
|
138
|
+
fitted = {
|
|
139
|
+
name: {key: ServiceCalibration.fit(sample) for key, sample in cells.items() if len(sample) >= minimum}
|
|
140
|
+
for name, cells in pools.items()
|
|
141
|
+
}
|
|
142
|
+
pooled = ServiceCalibration.fit(normalized) if len(normalized) >= 2 else None
|
|
143
|
+
audit = {
|
|
144
|
+
"history_count": len(needs),
|
|
145
|
+
"scaled_count": len(normalized),
|
|
146
|
+
"missing_scale_count": sum(s is None for s in scales),
|
|
147
|
+
"minimum": int(minimum),
|
|
148
|
+
"exact": exact,
|
|
149
|
+
"ratio": ratio,
|
|
150
|
+
"fitted_cells": {name: len(cells) for name, cells in fitted.items()},
|
|
151
|
+
}
|
|
152
|
+
return cls(absolute, pooled, fitted["coarse"], fitted["joint"], fitted["exact"], ratio, audit)
|
|
153
|
+
|
|
154
|
+
def predict(self, need, context, *, scale=None):
|
|
155
|
+
"""Return the complete predictive distribution and the fallback used."""
|
|
156
|
+
need, context = _need(need), _context(context)
|
|
157
|
+
if scale is not None:
|
|
158
|
+
_positive(scale, "scale")
|
|
159
|
+
if self.ratio and (scale is None or self.pooled is None):
|
|
160
|
+
distribution, level = self.absolute.distribution(need)
|
|
161
|
+
return EmpiricalPrediction(distribution, 1.0, f"absolute_{level}")
|
|
162
|
+
if not self.ratio and scale is not None:
|
|
163
|
+
raise ValueError("scale is only valid for a ratio fit")
|
|
164
|
+
factor = float(scale) if self.ratio else 1.0
|
|
165
|
+
group = int(np.searchsorted([1, 8, 32], need))
|
|
166
|
+
choices = (
|
|
167
|
+
(self.exact_joint.get((need, context)), "exact_context"),
|
|
168
|
+
(self.joint.get((group, context)), "coarse_context"),
|
|
169
|
+
(self.coarse.get(group), "coarse"),
|
|
170
|
+
(self.pooled, "pooled"),
|
|
171
|
+
)
|
|
172
|
+
for distribution, level in choices:
|
|
173
|
+
if distribution is not None:
|
|
174
|
+
return EmpiricalPrediction(distribution, factor, level)
|
|
175
|
+
raise ValueError("no predictive distribution available")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fitting a Markovian Arrival Process (MAP) to data.
|
|
3
|
+
|
|
4
|
+
The workhorse is the two-state MMPP (MMPP-2): a Poisson process whose rate
|
|
5
|
+
switches between two levels driven by a two-state Markov chain. It is fitted
|
|
6
|
+
by matching three interarrival statistics — the rate, the squared coefficient
|
|
7
|
+
of variation (SCV) and the lag-1 autocorrelation — for which the MAP has
|
|
8
|
+
closed-form expressions (see ``most_queue.random.map_ph``).
|
|
9
|
+
|
|
10
|
+
An MMPP is always overdispersed and positively correlated: SCV >= 1 and
|
|
11
|
+
lag-1 correlation >= 0. Targets outside this region cannot be represented by
|
|
12
|
+
an MMPP-2 and raise a clear error.
|
|
13
|
+
|
|
14
|
+
References:
|
|
15
|
+
Fischer W., Meier-Hellstern K. The Markov-modulated Poisson process (MMPP)
|
|
16
|
+
cookbook. Performance Evaluation, 18(2), 1993.
|
|
17
|
+
doi:10.1016/0166-5316(93)90035-s.
|
|
18
|
+
Heffes H., Lucantoni D. A Markov modulated characterization of packetized
|
|
19
|
+
voice and data traffic. IEEE JSAC, 4(6), 1986. doi:10.1109/JSAC.1986.1146428.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from scipy.optimize import least_squares
|
|
24
|
+
|
|
25
|
+
from most_queue.random.map_ph import MAP, MAPParams
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def map_statistics(params: MAPParams) -> tuple[float, float, float]:
|
|
29
|
+
"""
|
|
30
|
+
Return (rate, scv, lag1) of a MAP: fundamental arrival rate, squared
|
|
31
|
+
coefficient of variation and lag-1 autocorrelation of interarrival times.
|
|
32
|
+
"""
|
|
33
|
+
rate = MAP.arrival_rate(params)
|
|
34
|
+
moments = MAP.calc_theory_moments(params, 2)
|
|
35
|
+
m1, m2 = moments[0], moments[1]
|
|
36
|
+
scv = m2 / (m1 * m1) - 1.0
|
|
37
|
+
lag1 = MAP.lag_correlation(params, 1)
|
|
38
|
+
return rate, scv, lag1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _mmpp2_from_vector(x: np.ndarray) -> MAPParams:
|
|
42
|
+
"""Build an MMPP-2 from log-parameters x = log([l1, l2, q12, q21])."""
|
|
43
|
+
l1, l2, q12, q21 = np.exp(x)
|
|
44
|
+
return MAP.mmpp([l1, l2], np.array([[-q12, q12], [q21, -q21]]))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fit_mmpp2(
|
|
48
|
+
rate: float,
|
|
49
|
+
scv: float,
|
|
50
|
+
lag1: float,
|
|
51
|
+
n_restarts: int = 6,
|
|
52
|
+
tol: float = 1e-9,
|
|
53
|
+
) -> MAPParams:
|
|
54
|
+
"""
|
|
55
|
+
Fit an MMPP-2 matching the given interarrival rate, SCV and lag-1
|
|
56
|
+
autocorrelation.
|
|
57
|
+
|
|
58
|
+
:param rate: target fundamental arrival rate (> 0)
|
|
59
|
+
:param scv: target squared coefficient of variation (>= 1 for an MMPP)
|
|
60
|
+
:param lag1: target lag-1 autocorrelation (>= 0 for an MMPP)
|
|
61
|
+
:param n_restarts: number of random restarts for the local optimizer
|
|
62
|
+
:param tol: acceptable residual norm to stop early
|
|
63
|
+
:return: MAPParams of the fitted MMPP-2
|
|
64
|
+
:raises ValueError: if the target is outside the MMPP-representable region
|
|
65
|
+
"""
|
|
66
|
+
if rate <= 0:
|
|
67
|
+
raise ValueError(f"rate must be positive, got {rate}")
|
|
68
|
+
if scv < 1.0 - 1e-6:
|
|
69
|
+
raise ValueError(f"an MMPP-2 requires SCV >= 1 (overdispersed), got {scv:.4f}")
|
|
70
|
+
if lag1 < -1e-6:
|
|
71
|
+
raise ValueError(f"an MMPP-2 requires non-negative lag-1 correlation, got {lag1:.4f}")
|
|
72
|
+
|
|
73
|
+
target = np.array([rate, scv, lag1])
|
|
74
|
+
weights = np.array([1.0 / max(rate, 1e-9), 1.0, 1.0])
|
|
75
|
+
|
|
76
|
+
def residuals(x: np.ndarray) -> np.ndarray:
|
|
77
|
+
try:
|
|
78
|
+
stats = np.array(map_statistics(_mmpp2_from_vector(x)))
|
|
79
|
+
except (np.linalg.LinAlgError, ValueError):
|
|
80
|
+
return np.full(3, 1e3)
|
|
81
|
+
return (stats - target) * weights
|
|
82
|
+
|
|
83
|
+
rng = np.random.default_rng(12345)
|
|
84
|
+
best = np.log([rate, rate, 0.1 * rate + 1e-3, 0.1 * rate + 1e-3])
|
|
85
|
+
best_cost = np.inf
|
|
86
|
+
for k in range(max(1, n_restarts)):
|
|
87
|
+
if k == 0:
|
|
88
|
+
# deterministic heuristic start: rates spread by the overdispersion,
|
|
89
|
+
# slow switching to create the correlation
|
|
90
|
+
spread = 1.0 + np.sqrt(max(scv - 1.0, 0.0))
|
|
91
|
+
x0 = np.log([rate * spread, rate / spread, 0.1 * rate + 1e-3, 0.1 * rate + 1e-3])
|
|
92
|
+
else:
|
|
93
|
+
x0 = np.log(rate) + rng.uniform(-2.0, 2.0, size=4)
|
|
94
|
+
# trf (not lm): the 4-parameter MMPP-2 is under-determined by the
|
|
95
|
+
# 3 target statistics, so residuals < variables is expected
|
|
96
|
+
sol = least_squares(residuals, x0, method="trf", max_nfev=2000)
|
|
97
|
+
if sol.cost < best_cost:
|
|
98
|
+
best, best_cost = sol.x, sol.cost
|
|
99
|
+
if np.linalg.norm(sol.fun) < tol:
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
fitted = _mmpp2_from_vector(best)
|
|
103
|
+
achieved = map_statistics(fitted)
|
|
104
|
+
err = np.abs(np.array(achieved) - target) * weights
|
|
105
|
+
if np.max(err) > 1e-2:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"failed to fit MMPP-2 to (rate={rate:.4f}, scv={scv:.4f}, lag1={lag1:.4f}); "
|
|
108
|
+
f"achieved (rate={achieved[0]:.4f}, scv={achieved[1]:.4f}, lag1={achieved[2]:.4f}). "
|
|
109
|
+
"The target may be near the boundary of the MMPP-representable region."
|
|
110
|
+
)
|
|
111
|
+
return fitted
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def trace_statistics(interarrivals) -> tuple[float, float, float]:
|
|
115
|
+
"""
|
|
116
|
+
Empirical (rate, scv, lag1) of a sequence of interarrival times.
|
|
117
|
+
"""
|
|
118
|
+
x = np.asarray(interarrivals, dtype=float)
|
|
119
|
+
if x.size < 3:
|
|
120
|
+
raise ValueError("need at least 3 interarrival samples")
|
|
121
|
+
mean = x.mean()
|
|
122
|
+
scv = x.var() / (mean * mean)
|
|
123
|
+
lag1 = float(np.corrcoef(x[:-1], x[1:])[0, 1])
|
|
124
|
+
return 1.0 / mean, scv, lag1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def fit_map_from_trace(interarrivals, **kwargs) -> MAPParams:
|
|
128
|
+
"""
|
|
129
|
+
Fit an MMPP-2 to a trace of interarrival times by matching its empirical
|
|
130
|
+
rate, SCV and lag-1 autocorrelation.
|
|
131
|
+
|
|
132
|
+
:param interarrivals: sequence of interarrival times
|
|
133
|
+
:param kwargs: forwarded to :func:`fit_mmpp2`
|
|
134
|
+
:return: MAPParams of the fitted MMPP-2
|
|
135
|
+
"""
|
|
136
|
+
rate, scv, lag1 = trace_statistics(interarrivals)
|
|
137
|
+
return fit_mmpp2(rate, max(scv, 1.0), max(lag1, 0.0), **kwargs)
|
most_queue/random/map_ph.py
CHANGED
|
@@ -43,6 +43,54 @@ class MAPParams:
|
|
|
43
43
|
D1: np.ndarray # transitions generating an arrival, shape (m, m)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
@dataclass
|
|
47
|
+
class BMAPParams:
|
|
48
|
+
"""
|
|
49
|
+
Batch Markovian Arrival Process parameters: a list of matrices
|
|
50
|
+
D[0], D[1], D[2], ... where D[0] holds phase transitions without arrivals
|
|
51
|
+
and D[k] (k >= 1) holds transitions that generate a batch of k arrivals.
|
|
52
|
+
sum_k D[k] is the generator of the phase process.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
D: list # [D0, D1, D2, ...], each an (m, m) matrix
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def bmap_poisson_batch(rate: float, batch_probs: list) -> BMAPParams:
|
|
59
|
+
"""
|
|
60
|
+
Single-phase BMAP: batches arrive as a Poisson process of intensity
|
|
61
|
+
``rate`` with batch-size probabilities ``batch_probs`` (batch_probs[k-1] is
|
|
62
|
+
P(batch size = k)). Equivalent to the M^[X]/... batch arrival stream.
|
|
63
|
+
"""
|
|
64
|
+
p = np.asarray(batch_probs, dtype=float)
|
|
65
|
+
if abs(p.sum() - 1.0) > 1e-9 or np.any(p < 0):
|
|
66
|
+
raise ValueError("batch_probs must be a probability vector")
|
|
67
|
+
d = [np.array([[-rate]])]
|
|
68
|
+
for pk in p:
|
|
69
|
+
d.append(np.array([[rate * pk]]))
|
|
70
|
+
return BMAPParams(D=d)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bmap_from_map(params: MAPParams) -> BMAPParams:
|
|
74
|
+
"""A MAP as a BMAP with only size-1 batches (D[1] = D1)."""
|
|
75
|
+
return BMAPParams(D=[np.asarray(params.D0, dtype=float), np.asarray(params.D1, dtype=float)])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def bmap_arrival_rate(params: BMAPParams) -> float:
|
|
79
|
+
"""
|
|
80
|
+
Fundamental arrival rate of a BMAP: pi @ (sum_k k * D[k]) @ 1, where pi is
|
|
81
|
+
the stationary distribution of the phase generator sum_k D[k].
|
|
82
|
+
"""
|
|
83
|
+
mats = [np.asarray(x, dtype=float) for x in params.D]
|
|
84
|
+
gen = np.sum(mats, axis=0)
|
|
85
|
+
m = gen.shape[0]
|
|
86
|
+
a = np.vstack([gen.T, np.ones(m)])
|
|
87
|
+
b = np.zeros(m + 1)
|
|
88
|
+
b[-1] = 1.0
|
|
89
|
+
pi, *_ = np.linalg.lstsq(a, b, rcond=None)
|
|
90
|
+
batch_rate = np.sum([k * mats[k] for k in range(1, len(mats))], axis=0)
|
|
91
|
+
return float(pi @ batch_rate @ np.ones(m))
|
|
92
|
+
|
|
93
|
+
|
|
46
94
|
def _validate_ph(alpha: np.ndarray, T: np.ndarray) -> None:
|
|
47
95
|
if T.ndim != 2 or T.shape[0] != T.shape[1]:
|
|
48
96
|
raise ValueError("T must be a square matrix")
|