graphyco 0.1.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.
- graphyco/__init__.py +44 -0
- graphyco/bridge/__init__.py +32 -0
- graphyco/bridge/grad_bridge.py +430 -0
- graphyco/bridge/torch_bridge.py +191 -0
- graphyco/core/__init__.py +11 -0
- graphyco/core/evaluation.py +85 -0
- graphyco/core/graph.py +79 -0
- graphyco/core/invariants.py +64 -0
- graphyco/core/primitives.py +108 -0
- graphyco/observer/__init__.py +47 -0
- graphyco/observer/grad_eval.py +580 -0
- graphyco/observer/grad_observer.py +387 -0
- graphyco/tests/test_visualizer.py +185 -0
- graphyco/visualizer/__init__.py +29 -0
- graphyco/visualizer/__main__.py +106 -0
- graphyco/visualizer/app.py +382 -0
- graphyco/visualizer/diagnostics.py +234 -0
- graphyco/visualizer/gui.py +476 -0
- graphyco/visualizer/query.py +492 -0
- graphyco/visualizer/renderer.py +107 -0
- graphyco-0.1.0.dist-info/METADATA +494 -0
- graphyco-0.1.0.dist-info/RECORD +25 -0
- graphyco-0.1.0.dist-info/WHEEL +5 -0
- graphyco-0.1.0.dist-info/entry_points.txt +2 -0
- graphyco-0.1.0.dist-info/top_level.txt +1 -0
graphyco/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Graphyco: Computational graph topology and dynamic gradient-flow monitoring for PyTorch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .core.primitives import Edge, GraphState, Node, SCALE
|
|
6
|
+
from .core.evaluation import evaluate
|
|
7
|
+
from .core.invariants import validate_invariants, InvariantViolationError
|
|
8
|
+
from .bridge.torch_bridge import trace_to_graph, evaluate_model
|
|
9
|
+
from .bridge.grad_bridge import DynamicExecutionBridge, DynamicGraphState, LiveTrainingMonitor
|
|
10
|
+
from .visualizer.app import visualize, extract_benchmark_json
|
|
11
|
+
from .visualizer.query import QueryEngine, query_metrics
|
|
12
|
+
from .visualizer.diagnostics import LiveTrainingDiagnostics, GradientHealthStatus
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from .visualizer.gui import VisualizerDesktopApp, launch_desktop_app
|
|
16
|
+
except ImportError:
|
|
17
|
+
VisualizerDesktopApp = None # type: ignore
|
|
18
|
+
launch_desktop_app = None # type: ignore
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__name__ = "graphyco"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Edge",
|
|
25
|
+
"GraphState",
|
|
26
|
+
"Node",
|
|
27
|
+
"SCALE",
|
|
28
|
+
"evaluate",
|
|
29
|
+
"validate_invariants",
|
|
30
|
+
"InvariantViolationError",
|
|
31
|
+
"trace_to_graph",
|
|
32
|
+
"evaluate_model",
|
|
33
|
+
"DynamicExecutionBridge",
|
|
34
|
+
"DynamicGraphState",
|
|
35
|
+
"LiveTrainingMonitor",
|
|
36
|
+
"visualize",
|
|
37
|
+
"extract_benchmark_json",
|
|
38
|
+
"QueryEngine",
|
|
39
|
+
"query_metrics",
|
|
40
|
+
"LiveTrainingDiagnostics",
|
|
41
|
+
"GradientHealthStatus",
|
|
42
|
+
"VisualizerDesktopApp",
|
|
43
|
+
"launch_desktop_app",
|
|
44
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .torch_bridge import module_to_graph, trace_to_graph, evaluate_model, evaluate_model_dynamic
|
|
4
|
+
from .grad_bridge import DynamicGraphState, DynamicExecutionBridge, monitor_model, LiveTrainingMonitor
|
|
5
|
+
|
|
6
|
+
def visualize(*args, **kwargs):
|
|
7
|
+
from visualizer import visualize as _vis
|
|
8
|
+
return _vis(*args, **kwargs)
|
|
9
|
+
|
|
10
|
+
def launch_app(*args, **kwargs):
|
|
11
|
+
from visualizer import launch_app as _launch
|
|
12
|
+
return _launch(*args, **kwargs)
|
|
13
|
+
|
|
14
|
+
def extract_benchmark_json(*args, **kwargs):
|
|
15
|
+
from visualizer import extract_benchmark_json as _extract
|
|
16
|
+
return _extract(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"module_to_graph",
|
|
20
|
+
"trace_to_graph",
|
|
21
|
+
"evaluate_model",
|
|
22
|
+
"evaluate_model_dynamic",
|
|
23
|
+
"DynamicGraphState",
|
|
24
|
+
"DynamicExecutionBridge",
|
|
25
|
+
"monitor_model",
|
|
26
|
+
"LiveTrainingMonitor",
|
|
27
|
+
"visualize",
|
|
28
|
+
"launch_app",
|
|
29
|
+
"extract_benchmark_json",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
from torch.fx import GraphModule, symbolic_trace
|
|
13
|
+
|
|
14
|
+
from core.evaluation import evaluate, perturbation_profile
|
|
15
|
+
from core.primitives import Edge, GraphState, SCALE
|
|
16
|
+
from observer.grad_eval import evaluate_dynamic_flow
|
|
17
|
+
from observer.grad_observer import (
|
|
18
|
+
DynamicNodeRecord,
|
|
19
|
+
FXGradObserver,
|
|
20
|
+
ModuleGradObserver,
|
|
21
|
+
TensorStats,
|
|
22
|
+
compute_tensor_stats,
|
|
23
|
+
)
|
|
24
|
+
from .torch_bridge import module_to_graph, trace_to_graph
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ══════════════════════════════════════════════════════════════
|
|
28
|
+
# DYNAMIC GRAPH STATE REPRESENTATION: G_t = (V, E, X_t, A_t, G_t)
|
|
29
|
+
# ══════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DynamicGraphState:
|
|
33
|
+
"""Represents the complete state G_t = (V, E, X_t, A_t, G_t).
|
|
34
|
+
|
|
35
|
+
Preserves the structural representation G = (V, E) while attaching runtime
|
|
36
|
+
activation state A_t and backward gradient statistics G_t.
|
|
37
|
+
"""
|
|
38
|
+
graph_state: GraphState
|
|
39
|
+
step: int
|
|
40
|
+
node_records: Dict[str, DynamicNodeRecord]
|
|
41
|
+
edge_records: Dict[Tuple[str, str], TensorStats] = field(default_factory=dict)
|
|
42
|
+
static_evaluation: Dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
dynamic_evaluation: Dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
return {
|
|
47
|
+
"step": self.step,
|
|
48
|
+
"static_graph": self.graph_state.to_dict(),
|
|
49
|
+
"static_evaluation": self.static_evaluation,
|
|
50
|
+
"dynamic_evaluation": self.dynamic_evaluation,
|
|
51
|
+
"nodes": {nid: rec.to_dict() for nid, rec in sorted(self.node_records.items())},
|
|
52
|
+
"edges": {
|
|
53
|
+
f"{k[0]}->{k[1]}": stats.to_dict()
|
|
54
|
+
for k, stats in sorted(self.edge_records.items())
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def to_json(self, path: Optional[str] = None, indent: int = 2) -> str:
|
|
59
|
+
data = self.to_dict()
|
|
60
|
+
s = json.dumps(data, indent=indent)
|
|
61
|
+
if path:
|
|
62
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(s)
|
|
64
|
+
return s
|
|
65
|
+
|
|
66
|
+
def to_csv_nodes(self, path: Optional[str] = None) -> str:
|
|
67
|
+
output = io.StringIO()
|
|
68
|
+
writer = csv.writer(output)
|
|
69
|
+
writer.writerow([
|
|
70
|
+
"node_id", "node_type", "has_params",
|
|
71
|
+
"act_numel", "act_rms", "act_mean", "act_std", "act_zero_frac",
|
|
72
|
+
"grad_numel", "grad_rms", "grad_mean", "grad_std", "grad_zero_frac",
|
|
73
|
+
"ratio_grad_act", "category"
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
categories = self.dynamic_evaluation.get("correspondence", {}).get("node_categories", {})
|
|
77
|
+
|
|
78
|
+
for nid, rec in sorted(self.node_records.items()):
|
|
79
|
+
act = rec.activation
|
|
80
|
+
grad = rec.activation_gradient
|
|
81
|
+
writer.writerow([
|
|
82
|
+
nid,
|
|
83
|
+
rec.node_type,
|
|
84
|
+
rec.has_parameters,
|
|
85
|
+
act.numel if act else 0,
|
|
86
|
+
f"{act.rms:.6f}" if act else "",
|
|
87
|
+
f"{act.mean:.6f}" if act else "",
|
|
88
|
+
f"{act.std:.6f}" if act else "",
|
|
89
|
+
f"{act.zero_fraction:.4f}" if act else "",
|
|
90
|
+
grad.numel if grad else 0,
|
|
91
|
+
f"{grad.rms:.6f}" if grad else "",
|
|
92
|
+
f"{grad.mean:.6f}" if grad else "",
|
|
93
|
+
f"{grad.std:.6f}" if grad else "",
|
|
94
|
+
f"{grad.zero_fraction:.4f}" if grad else "",
|
|
95
|
+
f"{rec.ratio_grad_act:.6f}" if rec.ratio_grad_act is not None else "",
|
|
96
|
+
categories.get(nid, "Unknown"),
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
csv_str = output.getvalue()
|
|
100
|
+
if path:
|
|
101
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
102
|
+
f.write(csv_str)
|
|
103
|
+
return csv_str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ══════════════════════════════════════════════════════════════
|
|
107
|
+
# DYNAMIC EXECUTION BRIDGE
|
|
108
|
+
# ══════════════════════════════════════════════════════════════
|
|
109
|
+
|
|
110
|
+
class DynamicExecutionBridge:
|
|
111
|
+
"""Manages observational forward and backward graph execution.
|
|
112
|
+
|
|
113
|
+
Coordinates between PyTorch model evaluation, static graph extraction,
|
|
114
|
+
and dynamic runtime monitoring.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
model: nn.Module,
|
|
120
|
+
mode: str = "trace",
|
|
121
|
+
concrete_args: Optional[Dict] = None,
|
|
122
|
+
record_edges: bool = True,
|
|
123
|
+
):
|
|
124
|
+
self.model = model
|
|
125
|
+
self.mode = mode
|
|
126
|
+
self.concrete_args = concrete_args
|
|
127
|
+
self.record_edges = record_edges
|
|
128
|
+
|
|
129
|
+
# Extract static graph representation
|
|
130
|
+
if self.mode == "trace":
|
|
131
|
+
try:
|
|
132
|
+
self.static_state = trace_to_graph(model, concrete_args=concrete_args)
|
|
133
|
+
self.traced_module = symbolic_trace(model, concrete_args=concrete_args)
|
|
134
|
+
self.observer = FXGradObserver(self.traced_module, record_edges=record_edges)
|
|
135
|
+
except Exception:
|
|
136
|
+
# Fallback to module mode if FX symbolic trace fails
|
|
137
|
+
self.mode = "module"
|
|
138
|
+
self.static_state = module_to_graph(model)
|
|
139
|
+
self.traced_module = None
|
|
140
|
+
self.observer = ModuleGradObserver(model)
|
|
141
|
+
else:
|
|
142
|
+
self.static_state = module_to_graph(model)
|
|
143
|
+
self.traced_module = None
|
|
144
|
+
self.observer = ModuleGradObserver(model)
|
|
145
|
+
|
|
146
|
+
self.static_evaluation = evaluate(self.static_state)
|
|
147
|
+
self.static_profile = perturbation_profile(self.static_state)
|
|
148
|
+
self.trajectory: List[DynamicGraphState] = []
|
|
149
|
+
self.step_counter = 0
|
|
150
|
+
|
|
151
|
+
def step(
|
|
152
|
+
self,
|
|
153
|
+
inputs: Union[torch.Tensor, Tuple, Dict],
|
|
154
|
+
target: Optional[torch.Tensor] = None,
|
|
155
|
+
loss_fn: Optional[Callable] = None,
|
|
156
|
+
optimizer: Optional[torch.optim.Optimizer] = None,
|
|
157
|
+
) -> DynamicGraphState:
|
|
158
|
+
"""Executes a single observational monitoring step.
|
|
159
|
+
|
|
160
|
+
1. Forward pass (recording activations)
|
|
161
|
+
2. Loss evaluation
|
|
162
|
+
3. Backward pass (recording activation and edge gradients)
|
|
163
|
+
4. Parameter gradient capture
|
|
164
|
+
5. Flow statistics and metric evaluation
|
|
165
|
+
"""
|
|
166
|
+
self.step_counter += 1
|
|
167
|
+
self.observer.reset()
|
|
168
|
+
|
|
169
|
+
# Prepare inputs with autograd enabled
|
|
170
|
+
if isinstance(inputs, torch.Tensor):
|
|
171
|
+
if not inputs.requires_grad and inputs.is_floating_point():
|
|
172
|
+
inputs = inputs.clone().detach().requires_grad_(True)
|
|
173
|
+
args = (inputs,)
|
|
174
|
+
kwargs = {}
|
|
175
|
+
elif isinstance(inputs, tuple):
|
|
176
|
+
args = inputs
|
|
177
|
+
kwargs = {}
|
|
178
|
+
elif isinstance(inputs, dict):
|
|
179
|
+
args = ()
|
|
180
|
+
kwargs = inputs
|
|
181
|
+
else:
|
|
182
|
+
args = (inputs,)
|
|
183
|
+
kwargs = {}
|
|
184
|
+
|
|
185
|
+
# 1. Forward pass
|
|
186
|
+
if isinstance(self.observer, FXGradObserver):
|
|
187
|
+
output = self.observer.run(*args, **kwargs)
|
|
188
|
+
else:
|
|
189
|
+
output = self.model(*args, **kwargs)
|
|
190
|
+
|
|
191
|
+
# 2. Loss computation
|
|
192
|
+
if loss_fn is not None:
|
|
193
|
+
if target is not None:
|
|
194
|
+
loss = loss_fn(output, target)
|
|
195
|
+
else:
|
|
196
|
+
loss = loss_fn(output)
|
|
197
|
+
else:
|
|
198
|
+
if isinstance(output, torch.Tensor):
|
|
199
|
+
loss = output.sum()
|
|
200
|
+
elif isinstance(output, (tuple, list)) and len(output) > 0 and isinstance(output[0], torch.Tensor):
|
|
201
|
+
loss = output[0].sum()
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError("Model output is not a tensor; cannot compute default loss sum.")
|
|
204
|
+
|
|
205
|
+
# 3. Backward pass
|
|
206
|
+
if optimizer is not None:
|
|
207
|
+
optimizer.zero_grad()
|
|
208
|
+
loss.backward()
|
|
209
|
+
|
|
210
|
+
# 4. Parameter gradient capture
|
|
211
|
+
self.observer.capture_parameter_gradients()
|
|
212
|
+
|
|
213
|
+
# 5. Extract records
|
|
214
|
+
if isinstance(self.observer, FXGradObserver):
|
|
215
|
+
node_records, edge_records = self.observer.extract_records()
|
|
216
|
+
else:
|
|
217
|
+
node_records = self.observer.extract_records()
|
|
218
|
+
edge_records = {}
|
|
219
|
+
|
|
220
|
+
# Build trajectory for temporal analysis
|
|
221
|
+
trajectory_records = [s.node_records for s in self.trajectory] + [node_records]
|
|
222
|
+
|
|
223
|
+
# 6. Evaluate dynamic metrics
|
|
224
|
+
dynamic_eval = evaluate_dynamic_flow(
|
|
225
|
+
graph_state=self.static_state,
|
|
226
|
+
node_records=node_records,
|
|
227
|
+
edge_records=edge_records,
|
|
228
|
+
static_perturbation_profile=self.static_profile,
|
|
229
|
+
trajectory=trajectory_records,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
state = DynamicGraphState(
|
|
233
|
+
graph_state=self.static_state,
|
|
234
|
+
step=self.step_counter,
|
|
235
|
+
node_records=node_records,
|
|
236
|
+
edge_records=edge_records,
|
|
237
|
+
static_evaluation=self.static_evaluation,
|
|
238
|
+
dynamic_evaluation=dynamic_eval,
|
|
239
|
+
)
|
|
240
|
+
self.trajectory.append(state)
|
|
241
|
+
|
|
242
|
+
# 7. Optimizer step if requested
|
|
243
|
+
if optimizer is not None:
|
|
244
|
+
optimizer.step()
|
|
245
|
+
|
|
246
|
+
return state
|
|
247
|
+
|
|
248
|
+
def run_training_trajectory(
|
|
249
|
+
self,
|
|
250
|
+
batches: Union[List[torch.Tensor], List[Tuple[torch.Tensor, torch.Tensor]], Iterator],
|
|
251
|
+
loss_fn: Optional[Callable] = None,
|
|
252
|
+
optimizer: Optional[torch.optim.Optimizer] = None,
|
|
253
|
+
steps: Optional[int] = None,
|
|
254
|
+
) -> List[DynamicGraphState]:
|
|
255
|
+
"""Executes a multi-step training trajectory, recording temporal statistics."""
|
|
256
|
+
count = 0
|
|
257
|
+
for batch in batches:
|
|
258
|
+
if steps is not None and count >= steps:
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if isinstance(batch, (tuple, list)) and len(batch) == 2:
|
|
262
|
+
inp, tgt = batch
|
|
263
|
+
self.step(inputs=inp, target=tgt, loss_fn=loss_fn, optimizer=optimizer)
|
|
264
|
+
else:
|
|
265
|
+
self.step(inputs=batch, target=None, loss_fn=loss_fn, optimizer=optimizer)
|
|
266
|
+
|
|
267
|
+
count += 1
|
|
268
|
+
|
|
269
|
+
return self.trajectory
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ══════════════════════════════════════════════════════════════
|
|
273
|
+
# HIGH-LEVEL CONVENIENCE FUNCTION
|
|
274
|
+
# ══════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
def monitor_model(
|
|
277
|
+
model: nn.Module,
|
|
278
|
+
inputs: torch.Tensor,
|
|
279
|
+
loss_fn: Optional[Callable] = None,
|
|
280
|
+
mode: str = "trace",
|
|
281
|
+
concrete_args: Optional[Dict] = None,
|
|
282
|
+
) -> DynamicGraphState:
|
|
283
|
+
"""One-line convenience function to monitor a forward and backward pass on a PyTorch model."""
|
|
284
|
+
bridge = DynamicExecutionBridge(model, mode=mode, concrete_args=concrete_args)
|
|
285
|
+
return bridge.step(inputs=inputs, loss_fn=loss_fn)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ══════════════════════════════════════════════════════════════
|
|
289
|
+
# LIVE TRAINING MONITOR (FOR ACTIVE RUNNING TRAINING LOOPS)
|
|
290
|
+
# ══════════════════════════════════════════════════════════════
|
|
291
|
+
|
|
292
|
+
class LiveTrainingMonitor:
|
|
293
|
+
"""Non-intrusive live monitor for running PyTorch training loops.
|
|
294
|
+
|
|
295
|
+
Attaches directly to any custom training loop via context manager:
|
|
296
|
+
monitor = LiveTrainingMonitor(model, log_interval=10)
|
|
297
|
+
|
|
298
|
+
for step, (x, y) in enumerate(dataloader):
|
|
299
|
+
with monitor.observe(step):
|
|
300
|
+
optimizer.zero_grad()
|
|
301
|
+
out = model(x)
|
|
302
|
+
loss = criterion(out, y)
|
|
303
|
+
loss.backward()
|
|
304
|
+
optimizer.step()
|
|
305
|
+
|
|
306
|
+
# Retrieve dynamic gradient flow metrics in real time!
|
|
307
|
+
if monitor.has_new_data():
|
|
308
|
+
state = monitor.get_latest_state()
|
|
309
|
+
bneck = monitor.get_latest_bottleneck()
|
|
310
|
+
anomalies = monitor.get_live_anomalies()
|
|
311
|
+
stability = monitor.get_temporal_stability()
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
model: nn.Module,
|
|
317
|
+
mode: str = "trace",
|
|
318
|
+
concrete_args: Optional[Dict] = None,
|
|
319
|
+
log_interval: int = 1,
|
|
320
|
+
record_edges: bool = True,
|
|
321
|
+
):
|
|
322
|
+
self.model = model
|
|
323
|
+
self.mode = mode
|
|
324
|
+
self.concrete_args = concrete_args
|
|
325
|
+
self.log_interval = max(1, log_interval)
|
|
326
|
+
self.record_edges = record_edges
|
|
327
|
+
|
|
328
|
+
if self.mode == "trace":
|
|
329
|
+
try:
|
|
330
|
+
self.static_state = trace_to_graph(model, concrete_args=concrete_args)
|
|
331
|
+
self.traced_module = symbolic_trace(model, concrete_args=concrete_args)
|
|
332
|
+
self.observer = FXGradObserver(self.traced_module, record_edges=record_edges)
|
|
333
|
+
except Exception:
|
|
334
|
+
self.mode = "module"
|
|
335
|
+
self.static_state = module_to_graph(model)
|
|
336
|
+
self.traced_module = None
|
|
337
|
+
self.observer = ModuleGradObserver(model)
|
|
338
|
+
else:
|
|
339
|
+
self.static_state = module_to_graph(model)
|
|
340
|
+
self.traced_module = None
|
|
341
|
+
self.observer = ModuleGradObserver(model)
|
|
342
|
+
|
|
343
|
+
self.static_evaluation = evaluate(self.static_state)
|
|
344
|
+
self.static_profile = perturbation_profile(self.static_state)
|
|
345
|
+
self.trajectory: List[DynamicGraphState] = []
|
|
346
|
+
self.latest_state: Optional[DynamicGraphState] = None
|
|
347
|
+
self._new_data: bool = False
|
|
348
|
+
self._orig_forward: Optional[Callable] = None
|
|
349
|
+
|
|
350
|
+
def has_new_data(self) -> bool:
|
|
351
|
+
"""Returns True if the most recent step was observed and metrics were computed."""
|
|
352
|
+
return self._new_data
|
|
353
|
+
|
|
354
|
+
def get_latest_state(self) -> Optional[DynamicGraphState]:
|
|
355
|
+
"""Returns the most recent DynamicGraphState snapshot G_t."""
|
|
356
|
+
return self.latest_state
|
|
357
|
+
|
|
358
|
+
def get_latest_bottleneck(self) -> Dict[str, Any]:
|
|
359
|
+
"""Returns the current dynamic gradient bottleneck G_max and choke node."""
|
|
360
|
+
if not self.latest_state:
|
|
361
|
+
return {}
|
|
362
|
+
return self.latest_state.dynamic_evaluation.get("bottleneck", {})
|
|
363
|
+
|
|
364
|
+
def get_live_anomalies(self) -> Dict[str, Any]:
|
|
365
|
+
"""Returns any vanishing or exploding gradient nodes detected on the latest step."""
|
|
366
|
+
if not self.latest_state:
|
|
367
|
+
return {}
|
|
368
|
+
return self.latest_state.dynamic_evaluation.get("anomalies", {})
|
|
369
|
+
|
|
370
|
+
def get_temporal_stability(self) -> Dict[str, Any]:
|
|
371
|
+
"""Returns temporal CV and gradient stability metrics across all observed steps."""
|
|
372
|
+
if not self.latest_state:
|
|
373
|
+
return {}
|
|
374
|
+
return self.latest_state.dynamic_evaluation.get("temporal", {})
|
|
375
|
+
|
|
376
|
+
def export_live_json(self, path: str, indent: int = 2) -> None:
|
|
377
|
+
"""Exports the entire observed training trajectory to a JSON file for live visualization."""
|
|
378
|
+
data = {
|
|
379
|
+
"steps": [s.to_dict() for s in self.trajectory],
|
|
380
|
+
"latest_step": self.latest_state.step if self.latest_state else 0,
|
|
381
|
+
}
|
|
382
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
383
|
+
json.dump(data, f, indent=indent)
|
|
384
|
+
|
|
385
|
+
@contextmanager
|
|
386
|
+
def observe(self, step: int):
|
|
387
|
+
"""Context manager wrapping forward, loss, backward, and optimizer execution."""
|
|
388
|
+
active = (step % self.log_interval == 0)
|
|
389
|
+
self._new_data = False
|
|
390
|
+
if active:
|
|
391
|
+
self.observer.reset()
|
|
392
|
+
if self.mode == "trace":
|
|
393
|
+
self._orig_forward = self.model.forward
|
|
394
|
+
self.model.forward = lambda *args, **kwargs: self.observer.run(*args, **kwargs)
|
|
395
|
+
try:
|
|
396
|
+
yield self
|
|
397
|
+
finally:
|
|
398
|
+
if active:
|
|
399
|
+
if self.mode == "trace" and self._orig_forward is not None:
|
|
400
|
+
self.model.forward = self._orig_forward
|
|
401
|
+
self._orig_forward = None
|
|
402
|
+
|
|
403
|
+
self.observer.capture_parameter_gradients()
|
|
404
|
+
|
|
405
|
+
if hasattr(self.observer, "extract_records") and self.mode == "trace":
|
|
406
|
+
node_recs, edge_recs = self.observer.extract_records()
|
|
407
|
+
else:
|
|
408
|
+
node_recs = self.observer.extract_records()
|
|
409
|
+
edge_recs = {}
|
|
410
|
+
|
|
411
|
+
traj_recs = [s.node_records for s in self.trajectory] + [node_recs]
|
|
412
|
+
dyn_eval = evaluate_dynamic_flow(
|
|
413
|
+
self.static_state,
|
|
414
|
+
node_recs,
|
|
415
|
+
edge_records=edge_recs,
|
|
416
|
+
static_perturbation_profile=self.static_profile,
|
|
417
|
+
trajectory=traj_recs,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
self.latest_state = DynamicGraphState(
|
|
421
|
+
graph_state=self.static_state,
|
|
422
|
+
step=step,
|
|
423
|
+
node_records=node_recs,
|
|
424
|
+
edge_records=edge_recs,
|
|
425
|
+
static_evaluation=self.static_evaluation,
|
|
426
|
+
dynamic_evaluation=dyn_eval,
|
|
427
|
+
)
|
|
428
|
+
self.trajectory.append(self.latest_state)
|
|
429
|
+
self._new_data = True
|
|
430
|
+
|