flexsds 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.
Files changed (62) hide show
  1. flexsds/__init__.py +4 -0
  2. flexsds/assets/ptit_logo.png +0 -0
  3. flexsds/cli/__init__.py +1 -0
  4. flexsds/cli/banner.py +84 -0
  5. flexsds/cli/main.py +330 -0
  6. flexsds/cli/output.py +69 -0
  7. flexsds/cli/task_runner.py +171 -0
  8. flexsds/cli/wizard.py +360 -0
  9. flexsds/client/__init__.py +4 -0
  10. flexsds/client/chat.py +137 -0
  11. flexsds/client/client.py +107 -0
  12. flexsds/client/monitor.py +95 -0
  13. flexsds/core/__init__.py +44 -0
  14. flexsds/core/compatibility.py +442 -0
  15. flexsds/core/controller.py +502 -0
  16. flexsds/core/cost_model.py +530 -0
  17. flexsds/core/credentials.py +111 -0
  18. flexsds/core/hardware.py +77 -0
  19. flexsds/core/tree_topology.py +296 -0
  20. flexsds/profiles/profile_sm75_t4.json +62 -0
  21. flexsds/profiles/profile_sm89_l4.json +194 -0
  22. flexsds/server/__init__.py +1 -0
  23. flexsds/server/colab_runner.py +62 -0
  24. flexsds/server/local_runner.py +208 -0
  25. flexsds/server/modal_app.py +50 -0
  26. flexsds/server/modal_runner.py +98 -0
  27. flexsds/server/vscode_cli.py +82 -0
  28. flexsds-0.2.0.dist-info/METADATA +204 -0
  29. flexsds-0.2.0.dist-info/RECORD +62 -0
  30. flexsds-0.2.0.dist-info/WHEEL +5 -0
  31. flexsds-0.2.0.dist-info/entry_points.txt +2 -0
  32. flexsds-0.2.0.dist-info/top_level.txt +4 -0
  33. pipeline/entity_eval.py +171 -0
  34. pipeline/examples/node1_extractor_input_m0_04_001.txt +9 -0
  35. pipeline/examples/node1_extractor_output_m0_04_001.json +90 -0
  36. pipeline/extractor.py +502 -0
  37. pipeline/invoice_models.py +163 -0
  38. pipeline/node2_questions.py +30 -0
  39. pipeline/openai_compatible.py +87 -0
  40. pipeline/prompts/README.md +18 -0
  41. pipeline/prompts/node1_extractor_system.md +92 -0
  42. pipeline/prompts/node3_report_user.md +5 -0
  43. pipeline/report_writer.py +143 -0
  44. pipeline/schemas/invoice_extraction.schema.json +469 -0
  45. pipeline/sql_guard.py +140 -0
  46. pipeline/sqlite/schema.sql +219 -0
  47. pipeline/text_to_sql.py +170 -0
  48. pipeline/validation.py +166 -0
  49. pipeline/w1_workload.py +139 -0
  50. proposer/__init__.py +31 -0
  51. proposer/adapter.py +153 -0
  52. proposer/controller.py +460 -0
  53. proposer/cost_model.py +216 -0
  54. proposer/tree_adaptive.py +466 -0
  55. server/__init__.py +3 -0
  56. server/adaptive_scheduler.py +215 -0
  57. server/budget_planner.py +287 -0
  58. server/lineage.py +290 -0
  59. server/lineage_diagnostics.py +38 -0
  60. server/lineage_executor.py +129 -0
  61. server/lineage_startup.py +47 -0
  62. server/t4_rowwise_attention.py +262 -0
@@ -0,0 +1,502 @@
1
+ """TreeAdaptiveController v0.2 module with 2D tree topologies and dual-tier safety guards."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import math
6
+ from dataclasses import dataclass, field
7
+ from collections import deque
8
+ from typing import Dict, List, Optional, Tuple, Any
9
+
10
+ try:
11
+ from flexsds.core.cost_model import FlexCostModel, SolverResult
12
+ from flexsds.core.tree_topology import get_tree_topology, TreeTopology
13
+ except ModuleNotFoundError:
14
+ from src.flexsds.core.cost_model import FlexCostModel, SolverResult
15
+ from src.flexsds.core.tree_topology import get_tree_topology, TreeTopology
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Value threshold representing missing/sentinel VRAM readings (e.g. 1e9 from server planners)
20
+ SENTINEL_VRAM_THRESHOLD_MB = 1.0e8
21
+
22
+
23
+ @dataclass
24
+ class BudgetDecision:
25
+ """Represents a budget selection decision made by BudgetController."""
26
+ k: int
27
+ alpha_ema: float
28
+ expected_tokens: float
29
+ reason: str
30
+
31
+
32
+ class BudgetController:
33
+ """Base Adaptive Speculative Controller."""
34
+
35
+ def __init__(
36
+ self,
37
+ window_size: int = 10,
38
+ ema_beta: float = 0.8,
39
+ initial_alpha: float = 0.8,
40
+ fallback_threshold: float = 0.65,
41
+ high_threshold: float = 0.80,
42
+ candidate_budgets: Optional[List[int]] = None,
43
+ min_free_vram_mb: float = 1500.0,
44
+ **kwargs,
45
+ ):
46
+ self.window_size = window_size
47
+ self.ema_beta = ema_beta
48
+ self.alpha_ema = initial_alpha
49
+ self.initial_alpha = initial_alpha
50
+ self.fallback_threshold = fallback_threshold
51
+ self.high_threshold = high_threshold
52
+ self.candidate_budgets = candidate_budgets or [0, 2, 3, 4]
53
+ self.min_free_vram_mb = min_free_vram_mb
54
+ self._window: deque[float] = deque(maxlen=window_size)
55
+ self._history: List[dict] = []
56
+
57
+ def reset(self, initial_alpha: Optional[float] = None) -> None:
58
+ if initial_alpha is not None:
59
+ self.alpha_ema = initial_alpha
60
+ self.initial_alpha = initial_alpha
61
+ self._window.clear()
62
+ self._history.clear()
63
+
64
+
65
+ TREE_TOPOLOGIES: Dict[str, Dict[str, Any]] = {
66
+ "T0": {"num_nodes": 0, "k": 0, "depth": 0},
67
+ "T4": {"num_nodes": 4, "k": 2, "depth": 2},
68
+ "T8": {"num_nodes": 8, "k": 3, "depth": 3},
69
+ "T16": {"num_nodes": 16, "k": 4, "depth": 4},
70
+ "T32": {"num_nodes": 32, "k": 5, "depth": 5},
71
+ }
72
+
73
+
74
+ @dataclass
75
+ class TreeAdaptiveDecision(BudgetDecision):
76
+ """Represents an adaptive tree budget decision made by TreeAdaptiveController v0.2."""
77
+ topology_name: str = "T0"
78
+ num_nodes: int = 0
79
+ alpha_effective: float = 0.8
80
+ alpha_star: Optional[float] = None
81
+ speedup: float = 1.0
82
+ status: str = "ok" # "ok" | "memory_guard" | "concurrency_throttled" | "fallback_gate" | "probing" | "zero_crash_fallback"
83
+
84
+
85
+ class TreeAdaptiveController(BudgetController):
86
+ """Hardware-aware TreeAdaptive Speculative Controller v0.2.
87
+
88
+ Dynamically selects optimal 2D tree topology (T0, T4, T8, T16, T32) against the GPU cost model,
89
+ enforces dual-tier memory guard with hysteresis, throttles concurrency under high load,
90
+ and provides adaptive probing with zero-crash resilience.
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ cost_model: Optional[FlexCostModel] = None,
96
+ candidate_topologies: Optional[List[str]] = None,
97
+ min_free_vram_mb: float = 1500.0,
98
+ kv_cache_high_pct: float = 90.0,
99
+ kv_cache_recover_pct: float = 80.0,
100
+ cooldown_steps: int = 3,
101
+ window_size: int = 10,
102
+ ema_beta: float = 0.8,
103
+ initial_alpha: float = 0.8,
104
+ **kwargs,
105
+ ):
106
+ super().__init__(
107
+ window_size=window_size,
108
+ ema_beta=ema_beta,
109
+ initial_alpha=initial_alpha,
110
+ min_free_vram_mb=min_free_vram_mb,
111
+ **kwargs,
112
+ )
113
+ self.cost_model = cost_model or FlexCostModel()
114
+ self.candidate_topologies = candidate_topologies or ["T0", "T4", "T8", "T16", "T32"]
115
+ self.kv_cache_high_pct = kv_cache_high_pct
116
+ self.kv_cache_recover_pct = kv_cache_recover_pct
117
+ self.cooldown_steps = cooldown_steps
118
+
119
+ self._step_count: int = 0
120
+ self._observation_count: int = 0
121
+ self._fallback_steps: int = 0
122
+ self._memory_guard_active: bool = False
123
+ self._guard_trigger_source: str = "none"
124
+ self._cooldown_counter: int = 0
125
+ self._guard_reason: str = ""
126
+ self._last_selected_depth: int = 0
127
+ self._last_selected_topology: str = "T0"
128
+
129
+ @property
130
+ def step_count(self) -> int:
131
+ return self._step_count
132
+
133
+ @step_count.setter
134
+ def step_count(self, value: int) -> None:
135
+ self._step_count = value
136
+ self._observation_count = value
137
+
138
+ def reset(self, initial_alpha: Optional[float] = None) -> None:
139
+ """Resets controller state when beginning a new sequence/prompt."""
140
+ super().reset(initial_alpha=initial_alpha)
141
+ if initial_alpha is None:
142
+ self.alpha_ema = self.initial_alpha
143
+ self._step_count = 0
144
+ self._observation_count = 0
145
+ self._fallback_steps = 0
146
+ self._memory_guard_active = False
147
+ self._guard_trigger_source = "none"
148
+ self._cooldown_counter = 0
149
+ self._guard_reason = ""
150
+ self._last_selected_depth = 0
151
+ self._last_selected_topology = "T0"
152
+
153
+ @staticmethod
154
+ def _is_valid_vram(free_vram_mb: Optional[float]) -> bool:
155
+ """Validates that VRAM telemetry is present, finite, non-negative, and not a sentinel value."""
156
+ if free_vram_mb is None or math.isnan(free_vram_mb) or math.isinf(free_vram_mb):
157
+ return False
158
+ return 0.0 <= free_vram_mb < SENTINEL_VRAM_THRESHOLD_MB
159
+
160
+ @staticmethod
161
+ def _is_valid_kv(kv_cache_usage_pct: Optional[float]) -> bool:
162
+ """Validates that KV-cache usage percentage is present, finite, and in [0, 100]."""
163
+ if kv_cache_usage_pct is None or math.isnan(kv_cache_usage_pct) or math.isinf(kv_cache_usage_pct):
164
+ return False
165
+ return 0.0 <= kv_cache_usage_pct <= 100.0
166
+
167
+ def get_effective_alpha(self) -> float:
168
+ """Returns unbiased alpha taking into account warm-up bias correction and NaN self-healing."""
169
+ if math.isnan(self.alpha_ema) or math.isinf(self.alpha_ema):
170
+ self.alpha_ema = self.initial_alpha
171
+
172
+ if self._observation_count == 0:
173
+ return self.initial_alpha
174
+
175
+ beta_power = self.ema_beta ** self._observation_count
176
+ denominator = 1.0 - beta_power
177
+ if denominator <= 1e-6:
178
+ return self.alpha_ema
179
+
180
+ unbiased = (self.alpha_ema - self.initial_alpha * beta_power) / denominator
181
+ return min(max(unbiased, 0.0), 1.0)
182
+
183
+ def get_stats(self) -> dict:
184
+ """Exports runtime telemetry matching Interface A v0.2 contract."""
185
+ tree_budgets = [h["k"] for h in self._history]
186
+ topologies = [h.get("topology_name", "T0") for h in self._history]
187
+ return {
188
+ "alpha_ema": self.alpha_ema,
189
+ "effective_alpha": self.get_effective_alpha(),
190
+ "tree_budgets": tree_budgets,
191
+ "topologies": topologies,
192
+ "step_count": self._step_count,
193
+ "observation_count": self._observation_count,
194
+ "mean_k": (sum(tree_budgets) / len(tree_budgets)) if tree_budgets else 0.0,
195
+ "history": list(self._history),
196
+ "memory_guard_active": self._memory_guard_active,
197
+ "fallback_steps": self._fallback_steps,
198
+ }
199
+
200
+ def update(self, num_accepted: int, num_drafted: int) -> float:
201
+ """Updates rolling alpha telemetry from the latest step verification."""
202
+ self._step_count += 1
203
+
204
+ if num_drafted <= 0 or num_accepted < 0:
205
+ return self.alpha_ema
206
+
207
+ self._observation_count += 1
208
+ d = self._last_selected_depth
209
+
210
+ if d > 0 and num_drafted > d:
211
+ step_alpha = min(max(num_accepted / d, 0.0), 1.0)
212
+ else:
213
+ step_alpha = min(max(num_accepted / num_drafted, 0.0), 1.0)
214
+
215
+ self._window.append(step_alpha)
216
+ self.alpha_ema = self.ema_beta * self.alpha_ema + (1.0 - self.ema_beta) * step_alpha
217
+ return self.alpha_ema
218
+
219
+ def _get_probe_interval(self, batch_size: int) -> int:
220
+ """Returns load-aware probing interval T_probe(B)."""
221
+ if batch_size <= 4:
222
+ return 15
223
+ elif batch_size <= 16:
224
+ return 20
225
+ else:
226
+ return 30
227
+
228
+ def _get_throttled_candidates(self, batch_size: int) -> List[str]:
229
+ """Filters candidate topologies according to concurrency throttling tier."""
230
+ if batch_size <= 4:
231
+ allowed = ["T0", "T4", "T8", "T16", "T32"]
232
+ elif batch_size <= 12:
233
+ allowed = ["T0", "T4", "T8", "T16"]
234
+ elif batch_size <= 24:
235
+ allowed = ["T0", "T4", "T8"]
236
+ else:
237
+ allowed = ["T0", "T4"]
238
+
239
+ return [t for t in self.candidate_topologies if t in allowed]
240
+
241
+ def _make_decision(
242
+ self,
243
+ topology_name: str,
244
+ num_nodes: int,
245
+ alpha_effective: float,
246
+ alpha_star: Optional[float],
247
+ speedup: float,
248
+ status: str,
249
+ reason: str,
250
+ expected_tokens: float,
251
+ depth: int,
252
+ ) -> TreeAdaptiveDecision:
253
+ self._last_selected_depth = depth
254
+ self._last_selected_topology = topology_name
255
+ decision = TreeAdaptiveDecision(
256
+ k=num_nodes,
257
+ alpha_ema=self.alpha_ema,
258
+ expected_tokens=expected_tokens,
259
+ reason=reason,
260
+ topology_name=topology_name,
261
+ num_nodes=num_nodes,
262
+ alpha_effective=alpha_effective,
263
+ alpha_star=alpha_star,
264
+ speedup=speedup,
265
+ status=status,
266
+ )
267
+ self._history.append({
268
+ "k": num_nodes,
269
+ "topology_name": topology_name,
270
+ "alpha_effective": alpha_effective,
271
+ "alpha_star": alpha_star,
272
+ "speedup": speedup,
273
+ "status": status,
274
+ "reason": reason,
275
+ })
276
+ return decision
277
+
278
+ def select_budget(
279
+ self,
280
+ batch_size: int = 1,
281
+ free_vram_mb: float = 16000.0,
282
+ kv_cache_usage_pct: float = 0.0,
283
+ ) -> TreeAdaptiveDecision:
284
+ """Selects optimal tree topology considering memory, load, and dynamic break-even speedup."""
285
+ try:
286
+ # TẦNG 1: DUAL-TIER MEMORY GUARD
287
+ vram_valid = self._is_valid_vram(free_vram_mb)
288
+ kv_valid = self._is_valid_kv(kv_cache_usage_pct)
289
+
290
+ if not kv_valid:
291
+ is_vram_starved = False
292
+ is_kv_pressured = True
293
+ effective_kv_high = self.kv_cache_high_pct
294
+ effective_kv_recover = self.kv_cache_recover_pct
295
+ trigger_source = "kv"
296
+ else:
297
+ if vram_valid:
298
+ is_vram_starved = free_vram_mb < self.min_free_vram_mb
299
+ effective_kv_high = self.kv_cache_high_pct
300
+ effective_kv_recover = self.kv_cache_recover_pct
301
+ else:
302
+ is_vram_starved = False
303
+ effective_kv_high = 75.0
304
+ effective_kv_recover = 65.0
305
+
306
+ is_kv_pressured = kv_cache_usage_pct > effective_kv_high
307
+ if is_vram_starved and is_kv_pressured:
308
+ trigger_source = "both"
309
+ elif is_vram_starved:
310
+ trigger_source = "vram"
311
+ elif is_kv_pressured:
312
+ trigger_source = "kv"
313
+ else:
314
+ trigger_source = "none"
315
+
316
+ if is_vram_starved or is_kv_pressured:
317
+ self._memory_guard_active = True
318
+ self._guard_trigger_source = trigger_source
319
+ self._cooldown_counter = self.cooldown_steps
320
+ self._fallback_steps = 0
321
+ reason_parts = []
322
+ if is_vram_starved:
323
+ reason_parts.append(f"free VRAM: {free_vram_mb:.0f}MB < {self.min_free_vram_mb:.0f}MB")
324
+ if is_kv_pressured:
325
+ if not kv_valid:
326
+ reason_parts.append(f"invalid KV telemetry: {kv_cache_usage_pct}")
327
+ elif not vram_valid:
328
+ reason_parts.append(f"KV cache: {kv_cache_usage_pct:.1f}% > tight threshold {effective_kv_high:.1f}% (missing VRAM)")
329
+ else:
330
+ reason_parts.append(f"KV cache: {kv_cache_usage_pct:.1f}% > {effective_kv_high:.1f}%")
331
+ self._guard_reason = " | ".join(reason_parts)
332
+
333
+ return self._make_decision(
334
+ topology_name="T0",
335
+ num_nodes=0,
336
+ alpha_effective=self.get_effective_alpha(),
337
+ alpha_star=None,
338
+ speedup=1.0,
339
+ status="memory_guard",
340
+ reason=f"Memory guard active ({self._guard_reason})",
341
+ expected_tokens=1.0,
342
+ depth=0,
343
+ )
344
+
345
+ if self._memory_guard_active:
346
+ if self._guard_trigger_source in ["vram", "both"]:
347
+ can_recover = (
348
+ vram_valid
349
+ and (free_vram_mb >= self.min_free_vram_mb)
350
+ and kv_valid
351
+ and (kv_cache_usage_pct <= effective_kv_recover)
352
+ )
353
+ else:
354
+ can_recover = kv_valid and (kv_cache_usage_pct <= effective_kv_recover)
355
+
356
+ if self._cooldown_counter > 0:
357
+ self._cooldown_counter -= 1
358
+ self._fallback_steps = 0
359
+ return self._make_decision(
360
+ topology_name="T0",
361
+ num_nodes=0,
362
+ alpha_effective=self.get_effective_alpha(),
363
+ alpha_star=None,
364
+ speedup=1.0,
365
+ status="memory_guard",
366
+ reason=f"Memory guard cooldown ({self._cooldown_counter + 1} steps remaining; {self._guard_reason})",
367
+ expected_tokens=1.0,
368
+ depth=0,
369
+ )
370
+ else:
371
+ if can_recover:
372
+ self._memory_guard_active = False
373
+ self._guard_trigger_source = "none"
374
+ else:
375
+ self._fallback_steps = 0
376
+ return self._make_decision(
377
+ topology_name="T0",
378
+ num_nodes=0,
379
+ alpha_effective=self.get_effective_alpha(),
380
+ alpha_star=None,
381
+ speedup=1.0,
382
+ status="memory_guard",
383
+ reason=f"Memory guard holding (conditions not met: VRAM={free_vram_mb}MB valid={vram_valid}, KV={kv_cache_usage_pct}%)",
384
+ expected_tokens=1.0,
385
+ depth=0,
386
+ )
387
+
388
+ # TẦNG 2: UNBIASED ALPHA
389
+ alpha_effective = self.get_effective_alpha()
390
+ t4_result = self.cost_model.solve_alpha_star("T4", batch_size)
391
+ ref_alpha_star_t4 = t4_result.alpha_star if t4_result.is_feasible else None
392
+
393
+ # TẦNG 3: CONCURRENCY THROTTLING
394
+ allowed_candidates = self._get_throttled_candidates(batch_size)
395
+
396
+ if batch_size > 24 and alpha_effective < 0.85:
397
+ self._fallback_steps += 1
398
+ probe_interval = self._get_probe_interval(batch_size)
399
+ if self._fallback_steps >= probe_interval:
400
+ self._fallback_steps = 0
401
+ t4_topo = get_tree_topology("T4")
402
+ exp_tok = t4_topo.lookup_expected_acceptance(alpha_effective)
403
+ return self._make_decision(
404
+ topology_name="T4",
405
+ num_nodes=4,
406
+ alpha_effective=alpha_effective,
407
+ alpha_star=ref_alpha_star_t4,
408
+ speedup=1.0,
409
+ status="probing",
410
+ reason=f"Adaptive probing (interval {probe_interval} steps at B={batch_size})",
411
+ expected_tokens=exp_tok,
412
+ depth=4,
413
+ )
414
+ else:
415
+ return self._make_decision(
416
+ topology_name="T0",
417
+ num_nodes=0,
418
+ alpha_effective=alpha_effective,
419
+ alpha_star=ref_alpha_star_t4,
420
+ speedup=1.0,
421
+ status="concurrency_throttled",
422
+ reason=f"Concurrency throttling at B={batch_size} > 24 with alpha={alpha_effective:.3f} < 0.85",
423
+ expected_tokens=1.0,
424
+ depth=0,
425
+ )
426
+
427
+ # TẦNG 4: DYNAMIC FALLBACK GATE & SPEEDUP MAXIMIZATION
428
+ best_topo, best_speedup = self.cost_model.select_optimal_topology(
429
+ alpha=alpha_effective,
430
+ batch_size=batch_size,
431
+ candidate_topologies=allowed_candidates,
432
+ )
433
+
434
+ if best_topo == "T0" or best_speedup <= 1.0:
435
+ self._fallback_steps += 1
436
+ probe_interval = self._get_probe_interval(batch_size)
437
+ if self._fallback_steps >= probe_interval:
438
+ self._fallback_steps = 0
439
+ t4_topo = get_tree_topology("T4")
440
+ exp_tok = t4_topo.lookup_expected_acceptance(alpha_effective)
441
+ return self._make_decision(
442
+ topology_name="T4",
443
+ num_nodes=4,
444
+ alpha_effective=alpha_effective,
445
+ alpha_star=ref_alpha_star_t4,
446
+ speedup=1.0,
447
+ status="probing",
448
+ reason=f"Adaptive probing (interval {probe_interval} steps at B={batch_size})",
449
+ expected_tokens=exp_tok,
450
+ depth=4,
451
+ )
452
+ else:
453
+ return self._make_decision(
454
+ topology_name="T0",
455
+ num_nodes=0,
456
+ alpha_effective=alpha_effective,
457
+ alpha_star=ref_alpha_star_t4,
458
+ speedup=1.0,
459
+ status="fallback_gate",
460
+ reason=f"Dynamic fallback gate (max speedup {best_speedup:.3f} <= 1.0, step {self._fallback_steps}/{probe_interval})",
461
+ expected_tokens=1.0,
462
+ depth=0,
463
+ )
464
+
465
+ self._fallback_steps = 0
466
+ topo_obj = get_tree_topology(best_topo)
467
+ num_nodes = topo_obj.num_tokens
468
+ depth = topo_obj.depth
469
+ exp_tok = topo_obj.lookup_expected_acceptance(alpha_effective)
470
+ solver_res = self.cost_model.solve_alpha_star(best_topo, batch_size)
471
+ best_alpha_star = solver_res.alpha_star if solver_res.is_feasible else None
472
+
473
+ return self._make_decision(
474
+ topology_name=best_topo,
475
+ num_nodes=num_nodes,
476
+ alpha_effective=alpha_effective,
477
+ alpha_star=best_alpha_star,
478
+ speedup=best_speedup,
479
+ status="ok",
480
+ reason=f"Optimal topology {best_topo} (Speedup={best_speedup:.3f}x, alpha*={best_alpha_star})",
481
+ expected_tokens=exp_tok,
482
+ depth=depth,
483
+ )
484
+
485
+ except Exception as exc:
486
+ logger.warning("TreeAdaptiveController zero-crash triggered: %s", exc, exc_info=True)
487
+ return self._make_decision(
488
+ topology_name="T0",
489
+ num_nodes=0,
490
+ alpha_effective=self.initial_alpha,
491
+ alpha_star=None,
492
+ speedup=1.0,
493
+ status="zero_crash_fallback",
494
+ reason=f"Zero-crash fallback due to exception: {exc}",
495
+ expected_tokens=1.0,
496
+ depth=0,
497
+ )
498
+
499
+
500
+ # Alias HardwareAdaptiveController to reflect hardware-centric adaptive serving
501
+ HardwareAdaptiveController = TreeAdaptiveController
502
+ HardwareAdaptiveDecision = TreeAdaptiveDecision