Spaces:
Configuration error
Configuration error
Add rollout engine
Browse files
90-execution/distributed-inference/model-rollouts/rollout.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""rollout.py - async canary/progressive rollout state machine.
|
| 2 |
+
|
| 3 |
+
States: DRAFT -> APPROVED -> CANARY_PULL -> CANARY_VERIFY -> CANARY_SERVING
|
| 4 |
+
-> PROGRESSIVE_ROLLOUT -> COMPLETE (failures -> PAUSED/ROLLBACK)
|
| 5 |
+
Persist steps + audit to PostgreSQL; use Redis for exclusive transition locks.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import enum
|
| 9 |
+
from dataclasses import dataclass, field
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class State(str, enum.Enum):
|
| 13 |
+
DRAFT = "DRAFT"
|
| 14 |
+
APPROVED = "APPROVED"
|
| 15 |
+
CANARY_PULL = "CANARY_PULL"
|
| 16 |
+
CANARY_VERIFY = "CANARY_VERIFY"
|
| 17 |
+
CANARY_SERVING = "CANARY_SERVING"
|
| 18 |
+
PROGRESSIVE_ROLLOUT = "PROGRESSIVE_ROLLOUT"
|
| 19 |
+
COMPLETE = "COMPLETE"
|
| 20 |
+
PAUSED = "PAUSED"
|
| 21 |
+
ROLLBACK = "ROLLBACK"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class Rollout:
|
| 26 |
+
id: str
|
| 27 |
+
model_id: str
|
| 28 |
+
environment: str
|
| 29 |
+
plan: dict
|
| 30 |
+
state: State = State.DRAFT
|
| 31 |
+
history: list[str] = field(default_factory=list)
|
| 32 |
+
|
| 33 |
+
def transition(self, new: State, why: str = ""):
|
| 34 |
+
valid = {
|
| 35 |
+
State.DRAFT: {State.APPROVED},
|
| 36 |
+
State.APPROVED: {State.CANARY_PULL},
|
| 37 |
+
State.CANARY_PULL: {State.CANARY_VERIFY},
|
| 38 |
+
State.CANARY_VERIFY: {State.CANARY_SERVING, State.ROLLBACK},
|
| 39 |
+
State.CANARY_SERVING: {State.PROGRESSIVE_ROLLOUT, State.PAUSED, State.ROLLBACK},
|
| 40 |
+
State.PROGRESSIVE_ROLLOUT: {State.COMPLETE, State.PAUSED, State.ROLLBACK},
|
| 41 |
+
State.PAUSED: {State.PROGRESSIVE_ROLLOUT, State.ROLLBACK},
|
| 42 |
+
}.get(self.state, set())
|
| 43 |
+
if new not in valid:
|
| 44 |
+
raise ValueError(f"invalid transition {self.state} -> {new}")
|
| 45 |
+
self.state = new
|
| 46 |
+
self.history.append(f"{new.value}:{why}")
|
| 47 |
+
|
| 48 |
+
def gates_pass(self, metrics: dict) -> bool:
|
| 49 |
+
g = self.plan.get("quality_gates", {})
|
| 50 |
+
return all([
|
| 51 |
+
metrics.get("smoke_success_rate", 0) >= g.get("smoke_test_success_rate_min", 1.0),
|
| 52 |
+
metrics.get("error_rate", 1) <= g.get("error_rate_max", 0.01),
|
| 53 |
+
metrics.get("p95_ttft_ms", 1e9) <= g.get("p95_first_token_latency_ms_max", 3000),
|
| 54 |
+
metrics.get("p95_itl_ms", 1e9) <= g.get("p95_inter_token_latency_ms_max", 250),
|
| 55 |
+
])
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def approve(r: Rollout, role: str, ticket: str) -> Rollout:
|
| 59 |
+
need = r.plan.get("approval", {}).get("required_role", "model_operator")
|
| 60 |
+
if role != need or not ticket:
|
| 61 |
+
raise PermissionError(f"requires {need} + change ticket")
|
| 62 |
+
r.transition(State.APPROVED, f"approved_by={role} ticket={ticket}")
|
| 63 |
+
return r
|