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 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
+