Commit ·
6ad7bd8
1
Parent(s): 2343011
Add reward handling and normalization features to environment
Browse files- Introduced raw and normalized reward attributes in ClusterObservation and AntiAtroposEnvironment.
- Implemented reward normalization logic in stability module.
- Updated observability metrics to track raw and normalized rewards.
- Added tests for reward normalization and environment reward behavior.
- client.py +3 -0
- models.py +3 -0
- server/AntiAtropos_environment.py +28 -5
- stability.py +33 -0
- telemetry/observability.py +17 -3
client.py
CHANGED
|
@@ -111,6 +111,9 @@ class AntiAtroposEnv(
|
|
| 111 |
data_freshness_ms=obs_data.get("data_freshness_ms", 0),
|
| 112 |
action_ack_status=obs_data.get("action_ack_status", "success"),
|
| 113 |
choke_level=obs_data.get("choke_level", 0.0),
|
|
|
|
|
|
|
|
|
|
| 114 |
done=payload.get("done", False),
|
| 115 |
reward=payload.get("reward", 0.0),
|
| 116 |
)
|
|
|
|
| 111 |
data_freshness_ms=obs_data.get("data_freshness_ms", 0),
|
| 112 |
action_ack_status=obs_data.get("action_ack_status", "success"),
|
| 113 |
choke_level=obs_data.get("choke_level", 0.0),
|
| 114 |
+
raw_reward=obs_data.get("raw_reward", 0.0),
|
| 115 |
+
normalized_reward=obs_data.get("normalized_reward", 0.0),
|
| 116 |
+
reward_scale_version=obs_data.get("reward_scale_version", "sigmoid-v1"),
|
| 117 |
done=payload.get("done", False),
|
| 118 |
reward=payload.get("reward", 0.0),
|
| 119 |
)
|
models.py
CHANGED
|
@@ -155,6 +155,9 @@ class ClusterObservation(BaseModel):
|
|
| 155 |
action_id: str = ""
|
| 156 |
executor_latency_ms: float = Field(default=0.0, ge=0.0)
|
| 157 |
executor_error_code: str = ""
|
|
|
|
|
|
|
|
|
|
| 158 |
choke_level: float = 0.0
|
| 159 |
|
| 160 |
nodes: list[NodeObservation]
|
|
|
|
| 155 |
action_id: str = ""
|
| 156 |
executor_latency_ms: float = Field(default=0.0, ge=0.0)
|
| 157 |
executor_error_code: str = ""
|
| 158 |
+
raw_reward: float = 0.0
|
| 159 |
+
normalized_reward: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 160 |
+
reward_scale_version: str = "sigmoid-v1"
|
| 161 |
choke_level: float = 0.0
|
| 162 |
|
| 163 |
nodes: list[NodeObservation]
|
server/AntiAtropos_environment.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import time
|
| 2 |
import json
|
|
|
|
| 3 |
import logging
|
| 4 |
from uuid import uuid4
|
| 5 |
|
|
@@ -9,13 +10,13 @@ from openenv.core.env_server.types import State
|
|
| 9 |
try:
|
| 10 |
from ..models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode
|
| 11 |
from ..simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR
|
| 12 |
-
from ..stability import compute_lyapunov, compute_reward
|
| 13 |
from ..telemetry import PrometheusClient, get_observability_tracker
|
| 14 |
from ..control import KubernetesExecutor, ActionValidator
|
| 15 |
except ImportError:
|
| 16 |
from models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode # type: ignore[no-redef]
|
| 17 |
from simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR # type: ignore[no-redef]
|
| 18 |
-
from stability import compute_lyapunov, compute_reward # type: ignore[no-redef]
|
| 19 |
from telemetry import PrometheusClient, get_observability_tracker # type: ignore[no-redef]
|
| 20 |
from control import KubernetesExecutor, ActionValidator # type: ignore[no-redef]
|
| 21 |
|
|
@@ -34,6 +35,7 @@ MAX_REQUEST_RATE_NORM = 100.0
|
|
| 34 |
|
| 35 |
MAX_STEPS: int = 100 # Episode length
|
| 36 |
N_NODES: int = 5 # Cluster size
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
class AntiAtroposEnvironment(Environment):
|
|
@@ -70,6 +72,11 @@ class AntiAtroposEnvironment(Environment):
|
|
| 70 |
self._last_action_id: str = ""
|
| 71 |
self._last_executor_latency_ms: float = 0.0
|
| 72 |
self._last_executor_error_code: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
self._last_metric_time: float = 0.0
|
| 74 |
|
| 75 |
def reset(self, task_id: str = "task-1", mode: str = "simulated") -> ClusterObservation:
|
|
@@ -88,6 +95,8 @@ class AntiAtroposEnvironment(Environment):
|
|
| 88 |
self._last_action_id = ""
|
| 89 |
self._last_executor_latency_ms = 0.0
|
| 90 |
self._last_executor_error_code = ""
|
|
|
|
|
|
|
| 91 |
|
| 92 |
# Only set baseline metric time for hybrid/live to prevent misleading freshness in SIM
|
| 93 |
if self._mode in [EnvironmentMode.HYBRID, EnvironmentMode.LIVE]:
|
|
@@ -123,6 +132,8 @@ class AntiAtroposEnvironment(Environment):
|
|
| 123 |
self._last_action_id = str(uuid4())
|
| 124 |
self._last_executor_latency_ms = 0.0
|
| 125 |
self._last_executor_error_code = ""
|
|
|
|
|
|
|
| 126 |
|
| 127 |
# 1. Action Validation & Execution
|
| 128 |
valid_targets = [n["node_id"] for n in self._nodes_true]
|
|
@@ -196,7 +207,7 @@ class AntiAtroposEnvironment(Environment):
|
|
| 196 |
|
| 197 |
# 7. Compute scalar reward
|
| 198 |
cost = self._compute_cost(self._nodes_true)
|
| 199 |
-
|
| 200 |
v_prev=self._prev_lyapunov,
|
| 201 |
v_curr=current_lyapunov,
|
| 202 |
cost=cost,
|
|
@@ -205,6 +216,10 @@ class AntiAtroposEnvironment(Environment):
|
|
| 205 |
beta=BETA,
|
| 206 |
gamma=GAMMA
|
| 207 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
self._prev_lyapunov = current_lyapunov
|
| 210 |
|
|
@@ -225,7 +240,9 @@ class AntiAtroposEnvironment(Environment):
|
|
| 225 |
action_type=str(action.action_type.value),
|
| 226 |
target_node_id=str(action.target_node_id),
|
| 227 |
ack_status=self._action_ack_status,
|
| 228 |
-
|
|
|
|
|
|
|
| 229 |
lyapunov_energy=obs.lyapunov_energy,
|
| 230 |
total_queue_backlog=obs.total_queue_backlog,
|
| 231 |
average_latency_ms=obs.average_latency_ms,
|
|
@@ -248,7 +265,10 @@ class AntiAtroposEnvironment(Environment):
|
|
| 248 |
"action_ack_status": self._action_ack_status,
|
| 249 |
"executor_latency_ms": self._last_executor_latency_ms,
|
| 250 |
"executor_error_code": self._last_executor_error_code,
|
| 251 |
-
"
|
|
|
|
|
|
|
|
|
|
| 252 |
"lyapunov_energy": obs.lyapunov_energy,
|
| 253 |
"average_latency_ms_norm": obs.average_latency_ms,
|
| 254 |
"total_queue_backlog_norm": obs.total_queue_backlog,
|
|
@@ -364,6 +384,9 @@ class AntiAtroposEnvironment(Environment):
|
|
| 364 |
action_id=self._last_action_id,
|
| 365 |
executor_latency_ms=self._last_executor_latency_ms,
|
| 366 |
executor_error_code=self._last_executor_error_code,
|
|
|
|
|
|
|
|
|
|
| 367 |
choke_level=0.0,
|
| 368 |
done=False,
|
| 369 |
reward=0.0,
|
|
|
|
| 1 |
import time
|
| 2 |
import json
|
| 3 |
+
import os
|
| 4 |
import logging
|
| 5 |
from uuid import uuid4
|
| 6 |
|
|
|
|
| 10 |
try:
|
| 11 |
from ..models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode
|
| 12 |
from ..simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR
|
| 13 |
+
from ..stability import compute_lyapunov, compute_reward, normalize_reward, REWARD_SCALE_VERSION
|
| 14 |
from ..telemetry import PrometheusClient, get_observability_tracker
|
| 15 |
from ..control import KubernetesExecutor, ActionValidator
|
| 16 |
except ImportError:
|
| 17 |
from models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode # type: ignore[no-redef]
|
| 18 |
from simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR # type: ignore[no-redef]
|
| 19 |
+
from stability import compute_lyapunov, compute_reward, normalize_reward, REWARD_SCALE_VERSION # type: ignore[no-redef]
|
| 20 |
from telemetry import PrometheusClient, get_observability_tracker # type: ignore[no-redef]
|
| 21 |
from control import KubernetesExecutor, ActionValidator # type: ignore[no-redef]
|
| 22 |
|
|
|
|
| 35 |
|
| 36 |
MAX_STEPS: int = 100 # Episode length
|
| 37 |
N_NODES: int = 5 # Cluster size
|
| 38 |
+
REWARD_OUTPUT_MODES = {"normalized", "raw"}
|
| 39 |
|
| 40 |
|
| 41 |
class AntiAtroposEnvironment(Environment):
|
|
|
|
| 72 |
self._last_action_id: str = ""
|
| 73 |
self._last_executor_latency_ms: float = 0.0
|
| 74 |
self._last_executor_error_code: str = ""
|
| 75 |
+
self._last_raw_reward: float = 0.0
|
| 76 |
+
self._last_normalized_reward: float = 0.0
|
| 77 |
+
self._reward_output_mode: str = os.getenv("ANTIATROPOS_REWARD_OUTPUT_MODE", "normalized").strip().lower()
|
| 78 |
+
if self._reward_output_mode not in REWARD_OUTPUT_MODES:
|
| 79 |
+
self._reward_output_mode = "normalized"
|
| 80 |
self._last_metric_time: float = 0.0
|
| 81 |
|
| 82 |
def reset(self, task_id: str = "task-1", mode: str = "simulated") -> ClusterObservation:
|
|
|
|
| 95 |
self._last_action_id = ""
|
| 96 |
self._last_executor_latency_ms = 0.0
|
| 97 |
self._last_executor_error_code = ""
|
| 98 |
+
self._last_raw_reward = 0.0
|
| 99 |
+
self._last_normalized_reward = 0.0
|
| 100 |
|
| 101 |
# Only set baseline metric time for hybrid/live to prevent misleading freshness in SIM
|
| 102 |
if self._mode in [EnvironmentMode.HYBRID, EnvironmentMode.LIVE]:
|
|
|
|
| 132 |
self._last_action_id = str(uuid4())
|
| 133 |
self._last_executor_latency_ms = 0.0
|
| 134 |
self._last_executor_error_code = ""
|
| 135 |
+
self._last_raw_reward = 0.0
|
| 136 |
+
self._last_normalized_reward = 0.0
|
| 137 |
|
| 138 |
# 1. Action Validation & Execution
|
| 139 |
valid_targets = [n["node_id"] for n in self._nodes_true]
|
|
|
|
| 207 |
|
| 208 |
# 7. Compute scalar reward
|
| 209 |
cost = self._compute_cost(self._nodes_true)
|
| 210 |
+
raw_reward = compute_reward(
|
| 211 |
v_prev=self._prev_lyapunov,
|
| 212 |
v_curr=current_lyapunov,
|
| 213 |
cost=cost,
|
|
|
|
| 216 |
beta=BETA,
|
| 217 |
gamma=GAMMA
|
| 218 |
)
|
| 219 |
+
normalized_reward = normalize_reward(raw_reward)
|
| 220 |
+
reward = normalized_reward if self._reward_output_mode == "normalized" else raw_reward
|
| 221 |
+
self._last_raw_reward = raw_reward
|
| 222 |
+
self._last_normalized_reward = normalized_reward
|
| 223 |
|
| 224 |
self._prev_lyapunov = current_lyapunov
|
| 225 |
|
|
|
|
| 240 |
action_type=str(action.action_type.value),
|
| 241 |
target_node_id=str(action.target_node_id),
|
| 242 |
ack_status=self._action_ack_status,
|
| 243 |
+
reward_output=reward,
|
| 244 |
+
reward_raw=raw_reward,
|
| 245 |
+
reward_normalized=normalized_reward,
|
| 246 |
lyapunov_energy=obs.lyapunov_energy,
|
| 247 |
total_queue_backlog=obs.total_queue_backlog,
|
| 248 |
average_latency_ms=obs.average_latency_ms,
|
|
|
|
| 265 |
"action_ack_status": self._action_ack_status,
|
| 266 |
"executor_latency_ms": self._last_executor_latency_ms,
|
| 267 |
"executor_error_code": self._last_executor_error_code,
|
| 268 |
+
"reward_output": reward,
|
| 269 |
+
"reward_raw": raw_reward,
|
| 270 |
+
"reward_normalized": normalized_reward,
|
| 271 |
+
"reward_output_mode": self._reward_output_mode,
|
| 272 |
"lyapunov_energy": obs.lyapunov_energy,
|
| 273 |
"average_latency_ms_norm": obs.average_latency_ms,
|
| 274 |
"total_queue_backlog_norm": obs.total_queue_backlog,
|
|
|
|
| 384 |
action_id=self._last_action_id,
|
| 385 |
executor_latency_ms=self._last_executor_latency_ms,
|
| 386 |
executor_error_code=self._last_executor_error_code,
|
| 387 |
+
raw_reward=self._last_raw_reward,
|
| 388 |
+
normalized_reward=self._last_normalized_reward,
|
| 389 |
+
reward_scale_version=REWARD_SCALE_VERSION,
|
| 390 |
choke_level=0.0,
|
| 391 |
done=False,
|
| 392 |
reward=0.0,
|
stability.py
CHANGED
|
@@ -38,6 +38,7 @@ Key concepts implemented
|
|
| 38 |
|
| 39 |
from __future__ import annotations
|
| 40 |
|
|
|
|
| 41 |
import math
|
| 42 |
import statistics
|
| 43 |
from typing import Sequence
|
|
@@ -57,6 +58,16 @@ STABILITY_WINDOW: int = 10
|
|
| 57 |
trend-stable (V is on a decreasing trajectory)."""
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# ---------------------------------------------------------------------------
|
| 61 |
# Core Lyapunov functions
|
| 62 |
# ---------------------------------------------------------------------------
|
|
@@ -264,3 +275,25 @@ def compute_reward(
|
|
| 264 |
"""
|
| 265 |
delta_v = compute_drift(v_prev, v_curr)
|
| 266 |
return -(alpha * delta_v + beta * cost + gamma * sla_violation_step)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
from __future__ import annotations
|
| 40 |
|
| 41 |
+
import os
|
| 42 |
import math
|
| 43 |
import statistics
|
| 44 |
from typing import Sequence
|
|
|
|
| 58 |
trend-stable (V is on a decreasing trajectory)."""
|
| 59 |
|
| 60 |
|
| 61 |
+
# ---------------------------------------------------------------------------
|
| 62 |
+
# Reward normalisation defaults (env-overridable)
|
| 63 |
+
# ---------------------------------------------------------------------------
|
| 64 |
+
|
| 65 |
+
REWARD_NORM_MIDPOINT: float = float(os.getenv("ANTIATROPOS_REWARD_MIDPOINT", "0.0"))
|
| 66 |
+
REWARD_NORM_TEMPERATURE: float = float(os.getenv("ANTIATROPOS_REWARD_TEMPERATURE", "5.0"))
|
| 67 |
+
REWARD_NORM_EPS: float = float(os.getenv("ANTIATROPOS_REWARD_EPS", "1e-8"))
|
| 68 |
+
REWARD_SCALE_VERSION: str = "sigmoid-v1"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
# ---------------------------------------------------------------------------
|
| 72 |
# Core Lyapunov functions
|
| 73 |
# ---------------------------------------------------------------------------
|
|
|
|
| 275 |
"""
|
| 276 |
delta_v = compute_drift(v_prev, v_curr)
|
| 277 |
return -(alpha * delta_v + beta * cost + gamma * sla_violation_step)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def normalize_reward(
|
| 281 |
+
raw_reward: float,
|
| 282 |
+
midpoint: float = REWARD_NORM_MIDPOINT,
|
| 283 |
+
temperature: float = REWARD_NORM_TEMPERATURE,
|
| 284 |
+
eps: float = REWARD_NORM_EPS,
|
| 285 |
+
) -> float:
|
| 286 |
+
"""
|
| 287 |
+
Deterministically map raw reward to [0, 1] using a smooth sigmoid.
|
| 288 |
+
|
| 289 |
+
reward_01 = 1 / (1 + exp(-(raw_reward - midpoint) / temperature))
|
| 290 |
+
"""
|
| 291 |
+
temp = max(float(eps), abs(float(temperature)))
|
| 292 |
+
z = (float(raw_reward) - float(midpoint)) / temp
|
| 293 |
+
if z >= 0:
|
| 294 |
+
exp_neg = math.exp(-z)
|
| 295 |
+
out = 1.0 / (1.0 + exp_neg)
|
| 296 |
+
else:
|
| 297 |
+
exp_pos = math.exp(z)
|
| 298 |
+
out = exp_pos / (1.0 + exp_pos)
|
| 299 |
+
return min(1.0, max(0.0, float(out)))
|
telemetry/observability.py
CHANGED
|
@@ -47,7 +47,17 @@ class ObservabilityTracker:
|
|
| 47 |
|
| 48 |
self.reward_gauge = Gauge(
|
| 49 |
"antiatropos_reward",
|
| 50 |
-
"Latest reward value",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
["task_id", "mode"],
|
| 52 |
)
|
| 53 |
self.lyapunov_gauge = Gauge(
|
|
@@ -73,7 +83,9 @@ class ObservabilityTracker:
|
|
| 73 |
action_type: str,
|
| 74 |
target_node_id: str,
|
| 75 |
ack_status: str,
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
lyapunov_energy: float,
|
| 78 |
total_queue_backlog: float,
|
| 79 |
average_latency_ms: float,
|
|
@@ -93,7 +105,9 @@ class ObservabilityTracker:
|
|
| 93 |
target_node_id=target_node_id,
|
| 94 |
ack_class=ack_class,
|
| 95 |
).inc()
|
| 96 |
-
self.reward_gauge.labels(task_id=task_id, mode=mode).set(float(
|
|
|
|
|
|
|
| 97 |
self.lyapunov_gauge.labels(task_id=task_id, mode=mode).set(float(lyapunov_energy))
|
| 98 |
self.queue_gauge.labels(task_id=task_id, mode=mode).set(float(total_queue_backlog))
|
| 99 |
self.latency_gauge.labels(task_id=task_id, mode=mode).set(float(average_latency_ms))
|
|
|
|
| 47 |
|
| 48 |
self.reward_gauge = Gauge(
|
| 49 |
"antiatropos_reward",
|
| 50 |
+
"Latest output reward value (depends on reward output mode)",
|
| 51 |
+
["task_id", "mode"],
|
| 52 |
+
)
|
| 53 |
+
self.reward_raw_gauge = Gauge(
|
| 54 |
+
"antiatropos_reward_raw",
|
| 55 |
+
"Latest raw reward value before normalization",
|
| 56 |
+
["task_id", "mode"],
|
| 57 |
+
)
|
| 58 |
+
self.reward_normalized_gauge = Gauge(
|
| 59 |
+
"antiatropos_reward_normalized",
|
| 60 |
+
"Latest normalized reward value in [0,1]",
|
| 61 |
["task_id", "mode"],
|
| 62 |
)
|
| 63 |
self.lyapunov_gauge = Gauge(
|
|
|
|
| 83 |
action_type: str,
|
| 84 |
target_node_id: str,
|
| 85 |
ack_status: str,
|
| 86 |
+
reward_output: float,
|
| 87 |
+
reward_raw: float,
|
| 88 |
+
reward_normalized: float,
|
| 89 |
lyapunov_energy: float,
|
| 90 |
total_queue_backlog: float,
|
| 91 |
average_latency_ms: float,
|
|
|
|
| 105 |
target_node_id=target_node_id,
|
| 106 |
ack_class=ack_class,
|
| 107 |
).inc()
|
| 108 |
+
self.reward_gauge.labels(task_id=task_id, mode=mode).set(float(reward_output))
|
| 109 |
+
self.reward_raw_gauge.labels(task_id=task_id, mode=mode).set(float(reward_raw))
|
| 110 |
+
self.reward_normalized_gauge.labels(task_id=task_id, mode=mode).set(float(reward_normalized))
|
| 111 |
self.lyapunov_gauge.labels(task_id=task_id, mode=mode).set(float(lyapunov_energy))
|
| 112 |
self.queue_gauge.labels(task_id=task_id, mode=mode).set(float(total_queue_backlog))
|
| 113 |
self.latency_gauge.labels(task_id=task_id, mode=mode).set(float(average_latency_ms))
|