graflag 1.0.0__tar.gz
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.
- graflag-1.0.0/PKG-INFO +15 -0
- graflag-1.0.0/README.md +122 -0
- graflag-1.0.0/graflag/__init__.py +20 -0
- graflag-1.0.0/graflag/api.py +217 -0
- graflag-1.0.0/graflag/cli.py +321 -0
- graflag-1.0.0/graflag/config.py +128 -0
- graflag-1.0.0/graflag/core.py +735 -0
- graflag-1.0.0/graflag/devcluster/__init__.py +1 -0
- graflag-1.0.0/graflag/devcluster/cli.py +75 -0
- graflag-1.0.0/graflag/devcluster/deploy.sh +112 -0
- graflag-1.0.0/graflag/devcluster/docker-compose.yml +122 -0
- graflag-1.0.0/graflag/devcluster/hosts.yml +8 -0
- graflag-1.0.0/graflag/devcluster/manager/Dockerfile.manager +107 -0
- graflag-1.0.0/graflag/devcluster/manager/nvidia-setup.sh +174 -0
- graflag-1.0.0/graflag/devcluster/manager/registry-setup.sh +30 -0
- graflag-1.0.0/graflag/devcluster/manager/setup-nfs.sh +87 -0
- graflag-1.0.0/graflag/devcluster/worker/Dockerfile.worker +101 -0
- graflag-1.0.0/graflag/devcluster/worker/nvidia-setup.sh +171 -0
- graflag-1.0.0/graflag/devcluster/worker/registry-setup.sh +30 -0
- graflag-1.0.0/graflag/devcluster/worker/setup-nfs.sh +74 -0
- graflag-1.0.0/graflag/docker_ops.py +670 -0
- graflag-1.0.0/graflag/gui/__init__.py +1 -0
- graflag-1.0.0/graflag/gui/server.py +490 -0
- graflag-1.0.0/graflag/gui/static/css/style.css +880 -0
- graflag-1.0.0/graflag/gui/static/js/app.js +656 -0
- graflag-1.0.0/graflag/gui/static/js/components/ClusterStatus.js +58 -0
- graflag-1.0.0/graflag/gui/static/js/components/DataTable.js +124 -0
- graflag-1.0.0/graflag/gui/static/js/components/ExperimentModal.js +21 -0
- graflag-1.0.0/graflag/gui/static/js/components/RunForm.js +338 -0
- graflag-1.0.0/graflag/gui/templates/index.html +310 -0
- graflag-1.0.0/graflag/models.py +106 -0
- graflag-1.0.0/graflag/ssh.py +169 -0
- graflag-1.0.0/graflag/utils.py +36 -0
- graflag-1.0.0/graflag.egg-info/PKG-INFO +15 -0
- graflag-1.0.0/graflag.egg-info/SOURCES.txt +39 -0
- graflag-1.0.0/graflag.egg-info/dependency_links.txt +1 -0
- graflag-1.0.0/graflag.egg-info/entry_points.txt +2 -0
- graflag-1.0.0/graflag.egg-info/requires.txt +5 -0
- graflag-1.0.0/graflag.egg-info/top_level.txt +1 -0
- graflag-1.0.0/setup.cfg +4 -0
- graflag-1.0.0/setup.py +38 -0
graflag-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graflag
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Distributed benchmarking framework for Graph Anomaly Detection
|
|
5
|
+
Author: gbay7
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Requires-Dist: pyyaml>=5.0
|
|
8
|
+
Requires-Dist: docker>=6.0
|
|
9
|
+
Requires-Dist: Flask>=2.0.0
|
|
10
|
+
Requires-Dist: flask-socketio>=5.3.0
|
|
11
|
+
Requires-Dist: python-socketio>=5.9.0
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: requires-dist
|
|
14
|
+
Dynamic: requires-python
|
|
15
|
+
Dynamic: summary
|
graflag-1.0.0/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# GraFlag
|
|
2
|
+
|
|
3
|
+
Distributed benchmarking framework for Graph Anomaly Detection (GAD). Orchestrates experiments on Docker Swarm clusters with NFS-based shared storage.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install graflag
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/lias-laboratory/graflag.git
|
|
15
|
+
cd graflag
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This installs the `graflag` command (includes CLI, web GUI, and devcluster).
|
|
20
|
+
|
|
21
|
+
## Related Repositories
|
|
22
|
+
|
|
23
|
+
- [graflag-shared](https://github.com/lias-laboratory/graflag-shared) -- Methods, datasets, and shared libraries (NFS-mounted storage)
|
|
24
|
+
- [Documentation](https://lias-laboratory.github.io/graflag/) -- Full documentation (Sphinx)
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Set up the shared directory
|
|
29
|
+
|
|
30
|
+
Clone the shared storage repository on your NFS mount:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd /shared # or your NFS mount point
|
|
34
|
+
git clone https://github.com/lias-laboratory/graflag-shared.git .
|
|
35
|
+
git lfs pull # download dataset files
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Configure
|
|
39
|
+
|
|
40
|
+
Run the interactive setup wizard:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
graflag setup
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This stores configuration in `~/.config/graflag/config.env`. To reconfigure later:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
graflag setup --reconfigure
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or place a `.env` file in the working directory:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
MANAGER_IP=192.168.100.10
|
|
56
|
+
SSH_PORT=22
|
|
57
|
+
SSH_KEY=~/.ssh/id_ed25519
|
|
58
|
+
SHARED_DIR=/shared
|
|
59
|
+
HOSTS_FILE=hosts.yml
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Run an experiment
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
graflag run -m bond_dominant -d bond_inj_cora --build
|
|
66
|
+
graflag run -m taddy -d uci --params MAX_EPOCH=100 LEARNING_RATE=0.001
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Evaluate
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
graflag evaluate -e exp__bond_dominant__bond_inj_cora__20260309_120000
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
|---------|-------------|
|
|
79
|
+
| `graflag setup` | Interactive cluster configuration |
|
|
80
|
+
| `graflag run -m METHOD -d DATASET` | Run an experiment |
|
|
81
|
+
| `graflag status` | Show cluster status |
|
|
82
|
+
| `graflag list methods\|datasets\|experiments\|services` | List resources |
|
|
83
|
+
| `graflag logs -e EXP [-f]` | View experiment logs |
|
|
84
|
+
| `graflag stop -e EXP [--rm]` | Stop an experiment |
|
|
85
|
+
| `graflag evaluate -e EXP` | Evaluate experiment results |
|
|
86
|
+
| `graflag copy -s SRC -d DST [-r]` | Copy files to/from remote |
|
|
87
|
+
| `graflag sync [--lib] [--path PATH]` | Sync method or library |
|
|
88
|
+
| `graflag gui [--port PORT]` | Start web dashboard |
|
|
89
|
+
| `graflag devcluster --hosts FILE` | Deploy virtual cluster |
|
|
90
|
+
| `graflag devcluster --down` | Stop virtual cluster |
|
|
91
|
+
|
|
92
|
+
## Development Cluster
|
|
93
|
+
|
|
94
|
+
For local development without a physical cluster:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
graflag devcluster --hosts hosts.yml
|
|
98
|
+
graflag setup
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Dependencies
|
|
102
|
+
|
|
103
|
+
- `pyyaml` -- hosts.yml parsing for cluster setup
|
|
104
|
+
- `docker` -- Docker SDK for Python (service management via SSH tunnel)
|
|
105
|
+
- `flask`, `flask-socketio` -- Web GUI backend
|
|
106
|
+
|
|
107
|
+
## Module Structure
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
graflag/
|
|
111
|
+
__init__.py Package exports
|
|
112
|
+
cli.py Argument parsing, command dispatch, output formatting
|
|
113
|
+
core.py GraFlag orchestration class (returns structured data)
|
|
114
|
+
models.py Dataclass models (ClusterInfo, MethodInfo, ExperimentInfo, ...)
|
|
115
|
+
config.py Configuration loading (~/.config/graflag/config.env or .env)
|
|
116
|
+
ssh.py SSH and rsync file operations
|
|
117
|
+
docker_ops.py Docker Swarm operations via Docker SDK (SSH tunnel)
|
|
118
|
+
api.py Python API for GUI integration (wraps core)
|
|
119
|
+
utils.py Shared utility functions
|
|
120
|
+
gui/ Web dashboard subpackage (Flask + Vue.js)
|
|
121
|
+
devcluster/ Virtual cluster subpackage (Docker Compose)
|
|
122
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraFlag - Graph Anomaly Detection Benchmarking Tool
|
|
3
|
+
|
|
4
|
+
A tool for benchmarking Graph Anomaly Detection methods using Docker Swarm
|
|
5
|
+
across multiple nodes with shared NFS storage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import GraFlag, GraFlagError
|
|
9
|
+
from .config import GraflagConfig, CONFIG_FILE
|
|
10
|
+
from .models import (
|
|
11
|
+
ClusterInfo, MethodInfo, DatasetInfo, ExperimentInfo,
|
|
12
|
+
ExperimentResults, EvaluationResults, RunProgress,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__all__ = [
|
|
17
|
+
"GraFlag", "GraFlagError", "GraflagConfig", "CONFIG_FILE",
|
|
18
|
+
"ClusterInfo", "MethodInfo", "DatasetInfo", "ExperimentInfo",
|
|
19
|
+
"ExperimentResults", "EvaluationResults", "RunProgress",
|
|
20
|
+
]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraFlag Python API for GUI Integration.
|
|
3
|
+
|
|
4
|
+
Thin wrapper around GraFlag core that provides error-safe access
|
|
5
|
+
and returns structured dataclass objects suitable for GUI/web consumption.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List, Optional, Any, Callable
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from .core import GraFlag, GraFlagError
|
|
12
|
+
from .config import GraflagConfig
|
|
13
|
+
from .models import (
|
|
14
|
+
ClusterInfo, MethodInfo, DatasetInfo, ExperimentInfo,
|
|
15
|
+
ExperimentResults, EvaluationResults, RunProgress,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GraFlagAPI:
|
|
22
|
+
"""
|
|
23
|
+
High-level Python API for GraFlag operations.
|
|
24
|
+
|
|
25
|
+
All methods return structured data (dataclasses) and catch exceptions
|
|
26
|
+
to avoid crashing the GUI. Use the core GraFlag class directly for
|
|
27
|
+
CLI-style usage where exceptions should propagate.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
api = GraFlagAPI(config_file=".env")
|
|
31
|
+
|
|
32
|
+
cluster = api.get_cluster_info()
|
|
33
|
+
methods = api.list_methods()
|
|
34
|
+
experiments = api.list_experiments()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config_file: str = ".env", log_level: int = logging.INFO):
|
|
38
|
+
logging.basicConfig(level=log_level)
|
|
39
|
+
self.core = GraFlag(config_file)
|
|
40
|
+
self.config = self.core.config
|
|
41
|
+
|
|
42
|
+
# ========================================================================
|
|
43
|
+
# Cluster
|
|
44
|
+
# ========================================================================
|
|
45
|
+
|
|
46
|
+
def get_cluster_info(self) -> ClusterInfo:
|
|
47
|
+
"""Get cluster status information."""
|
|
48
|
+
return self.core.status()
|
|
49
|
+
|
|
50
|
+
def setup_cluster(self) -> Dict[str, Any]:
|
|
51
|
+
"""Setup GraFlag cluster."""
|
|
52
|
+
try:
|
|
53
|
+
self.core.setup()
|
|
54
|
+
return {"success": True, "message": "Cluster setup completed"}
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return {"success": False, "error": str(e)}
|
|
57
|
+
|
|
58
|
+
# ========================================================================
|
|
59
|
+
# Resources
|
|
60
|
+
# ========================================================================
|
|
61
|
+
|
|
62
|
+
def list_methods(self) -> List[MethodInfo]:
|
|
63
|
+
"""List available methods."""
|
|
64
|
+
try:
|
|
65
|
+
return self.core.list_methods()
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Error listing methods: {e}")
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
def get_method_details(self, method_name: str) -> Optional[MethodInfo]:
|
|
71
|
+
"""Get details for a specific method."""
|
|
72
|
+
try:
|
|
73
|
+
methods = self.core.list_methods()
|
|
74
|
+
for m in methods:
|
|
75
|
+
if m.name == method_name:
|
|
76
|
+
return m
|
|
77
|
+
return None
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error getting method details: {e}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def list_datasets(self) -> List[DatasetInfo]:
|
|
83
|
+
"""List available datasets."""
|
|
84
|
+
try:
|
|
85
|
+
return self.core.list_datasets()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Error listing datasets: {e}")
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
def list_experiments(self, limit: int = 50) -> List[ExperimentInfo]:
|
|
91
|
+
"""List recent experiments."""
|
|
92
|
+
try:
|
|
93
|
+
return self.core.list_experiments(limit=limit)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Error listing experiments: {e}")
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
def get_experiment_details(self, experiment_name: str) -> Optional[ExperimentInfo]:
|
|
99
|
+
"""Get details for a specific experiment."""
|
|
100
|
+
try:
|
|
101
|
+
experiments = self.core.list_experiments(limit=500)
|
|
102
|
+
for e in experiments:
|
|
103
|
+
if e.name == experiment_name:
|
|
104
|
+
return e
|
|
105
|
+
return None
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Error getting experiment details: {e}")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# ========================================================================
|
|
111
|
+
# Run
|
|
112
|
+
# ========================================================================
|
|
113
|
+
|
|
114
|
+
def run(
|
|
115
|
+
self,
|
|
116
|
+
method: str,
|
|
117
|
+
dataset: str,
|
|
118
|
+
tag: str = "latest",
|
|
119
|
+
build: bool = False,
|
|
120
|
+
gpu: bool = True,
|
|
121
|
+
method_params: Optional[Dict[str, Any]] = None,
|
|
122
|
+
on_progress: Optional[Callable[[RunProgress], None]] = None,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Run an experiment. Returns experiment name."""
|
|
125
|
+
return self.core.run(
|
|
126
|
+
method_name=method,
|
|
127
|
+
dataset=dataset,
|
|
128
|
+
tag=tag,
|
|
129
|
+
build=build,
|
|
130
|
+
gpu=gpu,
|
|
131
|
+
method_params=method_params or {},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ========================================================================
|
|
135
|
+
# Results
|
|
136
|
+
# ========================================================================
|
|
137
|
+
|
|
138
|
+
def get_experiment_results(self, experiment_name: str) -> Optional[ExperimentResults]:
|
|
139
|
+
"""Get experiment results."""
|
|
140
|
+
try:
|
|
141
|
+
return self.core.get_experiment_results(experiment_name)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Error getting results: {e}")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def get_evaluation_results(self, experiment_name: str) -> Optional[EvaluationResults]:
|
|
147
|
+
"""Get evaluation results."""
|
|
148
|
+
try:
|
|
149
|
+
return self.core.get_evaluation_results(experiment_name)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Error getting evaluation: {e}")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def evaluate_experiment(self, experiment_name: str) -> bool:
|
|
155
|
+
"""Run evaluation on an experiment."""
|
|
156
|
+
self.core.evaluate(experiment_name)
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
# ========================================================================
|
|
160
|
+
# Services
|
|
161
|
+
# ========================================================================
|
|
162
|
+
|
|
163
|
+
def list_running_services(self) -> List[Dict[str, str]]:
|
|
164
|
+
"""List running Docker services."""
|
|
165
|
+
try:
|
|
166
|
+
return self.core.list_services()
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error listing services: {e}")
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
def stop_experiment(self, experiment_name: str) -> bool:
|
|
172
|
+
"""Stop a running experiment."""
|
|
173
|
+
try:
|
|
174
|
+
self.core.stop(experiment_name)
|
|
175
|
+
return True
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Error stopping experiment: {e}")
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def delete_experiment(self, experiment_name: str) -> bool:
|
|
181
|
+
"""Stop and delete an experiment."""
|
|
182
|
+
try:
|
|
183
|
+
self.core.stop(experiment_name, remove=True)
|
|
184
|
+
return True
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error deleting experiment: {e}")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def get_experiment_logs(self, experiment_name: str, tail: int = 100) -> List[str]:
|
|
190
|
+
"""Get recent logs for an experiment."""
|
|
191
|
+
try:
|
|
192
|
+
return self.core.get_logs(experiment_name, tail=tail)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Error getting logs: {e}")
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
# ========================================================================
|
|
198
|
+
# File Operations
|
|
199
|
+
# ========================================================================
|
|
200
|
+
|
|
201
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
202
|
+
"""Download a file from remote shared directory."""
|
|
203
|
+
try:
|
|
204
|
+
self.core.copy_files(remote_path, local_path, recursive=False, from_remote=True)
|
|
205
|
+
return True
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"Error downloading file: {e}")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def download_directory(self, remote_path: str, local_path: str) -> bool:
|
|
211
|
+
"""Download a directory from remote shared directory."""
|
|
212
|
+
try:
|
|
213
|
+
self.core.copy_files(remote_path, local_path, recursive=True, from_remote=True)
|
|
214
|
+
return True
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error(f"Error downloading directory: {e}")
|
|
217
|
+
return False
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Command Line Interface for GraFlag."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import argparse
|
|
7
|
+
import logging
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .core import GraFlag, GraFlagError
|
|
12
|
+
from .config import get_config_path, init_config
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
16
|
+
)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
"""Main CLI interface."""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="GraFlag - Graph Anomaly Detection Benchmarking Tool",
|
|
24
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
25
|
+
epilog="""\
|
|
26
|
+
Examples:
|
|
27
|
+
graflag setup # Setup cluster
|
|
28
|
+
graflag setup --reconfigure # Re-run config wizard
|
|
29
|
+
graflag run --method Dummy --dataset Cora # Run experiment with GPU
|
|
30
|
+
graflag run -m taddy -d uci --build --params MAX_EPOCH=100 LEARNING_RATE=0.001
|
|
31
|
+
graflag run -m DeepWalk -d CiteSeer --no-gpu # Run without GPU
|
|
32
|
+
graflag run --from-config ./experiments/exp__method__dataset__timestamp/service_config.json
|
|
33
|
+
graflag status # Show cluster status
|
|
34
|
+
graflag list methods # List available methods
|
|
35
|
+
graflag list services # List running services
|
|
36
|
+
graflag logs -e exp__dummy__cora__20250924_161245 # Show logs
|
|
37
|
+
graflag logs -e exp__dummy__cora__20250924_161245 -f # Follow logs
|
|
38
|
+
graflag stop -e exp__dummy__cora__20250924_161245 # Stop experiment
|
|
39
|
+
graflag evaluate -e exp__generaldyg__btc_alpha__20251211_120000 # Evaluate
|
|
40
|
+
graflag copy -s ./data -d datasets -r # Copy to remote
|
|
41
|
+
graflag copy --from-remote -s experiments/exp -d ./local # Copy from remote
|
|
42
|
+
graflag sync # Sync current method dir
|
|
43
|
+
graflag sync --lib --path ./my-lib/ # Sync a shared library
|
|
44
|
+
graflag gui # Start web dashboard
|
|
45
|
+
graflag gui --port 8080 --debug # GUI on custom port
|
|
46
|
+
graflag devcluster --hosts hosts.yml # Deploy virtual cluster
|
|
47
|
+
graflag devcluster --hosts hosts.yml --pubkey ~/.ssh/id_rsa.pub
|
|
48
|
+
graflag devcluster --down # Stop and remove cluster
|
|
49
|
+
""",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"command",
|
|
54
|
+
choices=["setup", "run", "status", "list", "copy", "logs", "stop", "evaluate", "sync", "gui", "devcluster"],
|
|
55
|
+
help="Command to execute",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"subcommand", nargs="?",
|
|
59
|
+
choices=["methods", "datasets", "experiments", "services"],
|
|
60
|
+
help="Subcommand for list command",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument("--method", "-m", help="Method name for run")
|
|
63
|
+
parser.add_argument("--dataset", "-d", help="Dataset name for run")
|
|
64
|
+
parser.add_argument("--tag", "-t", default="latest", help="Docker image tag (default: latest)")
|
|
65
|
+
parser.add_argument("--build", "-b", action="store_true", help="Build image before running")
|
|
66
|
+
parser.add_argument("--config", "-c", default=".env", help="Configuration file (default: .env)")
|
|
67
|
+
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
|
|
68
|
+
parser.add_argument("--source", "-s", nargs='+', help="Source path(s) for copy command")
|
|
69
|
+
parser.add_argument("--dest", help="Destination path for copy command")
|
|
70
|
+
parser.add_argument("--recursive", "-r", action="store_true", help="Copy directories recursively")
|
|
71
|
+
parser.add_argument("--from-remote", action="store_true", help="Copy from remote to local")
|
|
72
|
+
parser.add_argument("--experiment", "-e", help="Experiment name for logs/stop commands")
|
|
73
|
+
parser.add_argument("--follow", "-f", action="store_true", help="Follow log output")
|
|
74
|
+
parser.add_argument("--rm", action="store_true", help="Also delete experiment directory on stop")
|
|
75
|
+
parser.add_argument("--tee", help="Save logs to file while displaying")
|
|
76
|
+
parser.add_argument("--gpu", "-g", action="store_true", default=True, help="Enable GPU (default: True)")
|
|
77
|
+
parser.add_argument("--no-gpu", action="store_false", dest="gpu", help="Disable GPU")
|
|
78
|
+
parser.add_argument("--params", "-p", nargs='+', metavar="KEY=VALUE", help="Method parameters")
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--from-config", metavar="CONFIG_FILE",
|
|
81
|
+
help="Load method/dataset/params from a config file",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument("--lib", action="store_true", help="Sync as a shared library")
|
|
84
|
+
parser.add_argument("--path", default=None, help="Local path for sync command")
|
|
85
|
+
# GUI args
|
|
86
|
+
parser.add_argument("--host", default="0.0.0.0", help="GUI server host (default: 0.0.0.0)")
|
|
87
|
+
parser.add_argument("--port", default=5000, type=int, help="GUI server port (default: 5000)")
|
|
88
|
+
parser.add_argument("--debug", action="store_true", help="Enable GUI debug mode")
|
|
89
|
+
# Devcluster args
|
|
90
|
+
parser.add_argument("--hosts", default=None, help="Path to hosts.yml for devcluster")
|
|
91
|
+
parser.add_argument("--pubkey", default=None, help="Path to SSH public key for devcluster")
|
|
92
|
+
parser.add_argument("--down", action="store_true", help="Stop and remove devcluster")
|
|
93
|
+
parser.add_argument("--reconfigure", action="store_true", help="Re-run configuration wizard for setup")
|
|
94
|
+
|
|
95
|
+
args = parser.parse_args()
|
|
96
|
+
|
|
97
|
+
if args.verbose:
|
|
98
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# GUI: start web dashboard (doesn't need GraFlag instance directly)
|
|
102
|
+
if args.command == "gui":
|
|
103
|
+
from .gui.server import serve
|
|
104
|
+
serve(args.config, args.host, args.port, args.debug)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# Devcluster: deploy or tear down virtual cluster
|
|
108
|
+
if args.command == "devcluster":
|
|
109
|
+
if not args.hosts and not args.down:
|
|
110
|
+
parser.error("devcluster requires --hosts <path-to-hosts.yml> or --down")
|
|
111
|
+
from .devcluster.cli import main as devcluster_main
|
|
112
|
+
devcluster_main(args.hosts, args.pubkey, args.down)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Setup: create or update config interactively
|
|
116
|
+
if args.command == "setup":
|
|
117
|
+
config_path = get_config_path(args.config)
|
|
118
|
+
if not config_path.exists() or args.reconfigure:
|
|
119
|
+
init_config()
|
|
120
|
+
|
|
121
|
+
gf = GraFlag(config_file=args.config)
|
|
122
|
+
|
|
123
|
+
if args.command == "setup":
|
|
124
|
+
gf.setup()
|
|
125
|
+
# Show status after setup
|
|
126
|
+
_print_status(gf.status())
|
|
127
|
+
|
|
128
|
+
elif args.command == "run":
|
|
129
|
+
method, dataset, method_params = _parse_run_args(args, parser)
|
|
130
|
+
gf.run(method, dataset, args.tag, args.build, args.gpu, method_params)
|
|
131
|
+
|
|
132
|
+
elif args.command == "status":
|
|
133
|
+
_print_status(gf.status())
|
|
134
|
+
|
|
135
|
+
elif args.command == "list":
|
|
136
|
+
if args.subcommand == "methods":
|
|
137
|
+
_print_methods(gf.list_methods())
|
|
138
|
+
elif args.subcommand == "datasets":
|
|
139
|
+
_print_datasets(gf.list_datasets())
|
|
140
|
+
elif args.subcommand == "experiments":
|
|
141
|
+
_print_experiments(gf.list_experiments())
|
|
142
|
+
elif args.subcommand == "services":
|
|
143
|
+
_print_services(gf.list_services())
|
|
144
|
+
else:
|
|
145
|
+
parser.error("list command requires subcommand: methods, datasets, experiments, or services")
|
|
146
|
+
|
|
147
|
+
elif args.command == "copy":
|
|
148
|
+
if not args.source or not args.dest:
|
|
149
|
+
parser.error("copy command requires --source and --dest")
|
|
150
|
+
gf.copy_files(args.source, args.dest, args.recursive, args.from_remote)
|
|
151
|
+
|
|
152
|
+
elif args.command == "logs":
|
|
153
|
+
if not args.experiment:
|
|
154
|
+
parser.error("logs command requires --experiment")
|
|
155
|
+
if args.follow:
|
|
156
|
+
gf.follow_logs(args.experiment, args.tee)
|
|
157
|
+
else:
|
|
158
|
+
gf.show_logs(args.experiment, args.tee)
|
|
159
|
+
|
|
160
|
+
elif args.command == "stop":
|
|
161
|
+
if not args.experiment:
|
|
162
|
+
parser.error("stop command requires --experiment")
|
|
163
|
+
gf.stop(args.experiment, remove=args.rm)
|
|
164
|
+
|
|
165
|
+
elif args.command == "evaluate":
|
|
166
|
+
if not args.experiment:
|
|
167
|
+
parser.error("evaluate command requires --experiment")
|
|
168
|
+
gf.evaluate(args.experiment)
|
|
169
|
+
|
|
170
|
+
elif args.command == "sync":
|
|
171
|
+
local_path = args.path or os.getcwd()
|
|
172
|
+
gf.sync(local_path, is_lib=args.lib)
|
|
173
|
+
|
|
174
|
+
except GraFlagError as e:
|
|
175
|
+
logger.error(str(e))
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
except KeyboardInterrupt:
|
|
178
|
+
logger.info("Interrupted by user")
|
|
179
|
+
sys.exit(0)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Unexpected error: {e}")
|
|
182
|
+
if args.verbose:
|
|
183
|
+
traceback.print_exc()
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ============================================================================
|
|
188
|
+
# Output Formatting
|
|
189
|
+
# ============================================================================
|
|
190
|
+
|
|
191
|
+
def _parse_run_args(args, parser):
|
|
192
|
+
"""Parse run arguments from CLI args."""
|
|
193
|
+
method = args.method
|
|
194
|
+
dataset = args.dataset
|
|
195
|
+
method_params = {}
|
|
196
|
+
|
|
197
|
+
if args.from_config:
|
|
198
|
+
config_path = Path(args.from_config)
|
|
199
|
+
if not config_path.exists():
|
|
200
|
+
parser.error(f"Config file not found: {args.from_config}")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
with open(config_path, 'r') as f:
|
|
204
|
+
config = json.load(f)
|
|
205
|
+
except json.JSONDecodeError as e:
|
|
206
|
+
parser.error(f"Invalid JSON in config file: {e}")
|
|
207
|
+
|
|
208
|
+
if not method:
|
|
209
|
+
method = config.get('method_name')
|
|
210
|
+
if not dataset:
|
|
211
|
+
dataset = config.get('dataset')
|
|
212
|
+
|
|
213
|
+
env_contents = config.get('env_contents', {})
|
|
214
|
+
for key, value in env_contents.items():
|
|
215
|
+
if key.startswith('_'):
|
|
216
|
+
method_params[key[1:]] = str(value)
|
|
217
|
+
|
|
218
|
+
if args.params:
|
|
219
|
+
for param in args.params:
|
|
220
|
+
if '=' not in param:
|
|
221
|
+
parser.error(f"Invalid parameter format: {param}. Use KEY=VALUE.")
|
|
222
|
+
key, value = param.split('=', 1)
|
|
223
|
+
method_params[key] = value
|
|
224
|
+
|
|
225
|
+
if not method or not dataset:
|
|
226
|
+
parser.error("run requires --method and --dataset (or --from-config)")
|
|
227
|
+
|
|
228
|
+
return method, dataset, method_params
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _print_status(cluster_info):
|
|
232
|
+
"""Format and print cluster status."""
|
|
233
|
+
if cluster_info.error:
|
|
234
|
+
print(f"[ERROR] {cluster_info.error}")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
print(f"\n[INFO] Manager: {cluster_info.manager_ip}")
|
|
238
|
+
print(f"[INFO] Swarm: {'active' if cluster_info.swarm_initialized else 'inactive'}")
|
|
239
|
+
|
|
240
|
+
if cluster_info.worker_nodes:
|
|
241
|
+
print("\n[INFO] Nodes:")
|
|
242
|
+
for node in cluster_info.worker_nodes:
|
|
243
|
+
role = "manager" if node.get('is_manager') else "worker"
|
|
244
|
+
print(f" - {node['hostname']}: {node['status']} ({role}, {node['availability']})")
|
|
245
|
+
|
|
246
|
+
if cluster_info.services:
|
|
247
|
+
print(f"\n[INFO] Running Services:")
|
|
248
|
+
print(f" {'NAME':<50} {'REPLICAS':<15} {'IMAGE':<30}")
|
|
249
|
+
print(" " + "-" * 95)
|
|
250
|
+
for svc in cluster_info.services:
|
|
251
|
+
name = svc['name'][:49]
|
|
252
|
+
replicas = svc.get('replicas', '')[:14]
|
|
253
|
+
image = svc.get('image', '')[:29]
|
|
254
|
+
print(f" {name:<50} {replicas:<15} {image:<30}")
|
|
255
|
+
else:
|
|
256
|
+
print("\n[INFO] Running Services: None")
|
|
257
|
+
|
|
258
|
+
print(f"\n[INFO] Shared Directory: {cluster_info.shared_dir}")
|
|
259
|
+
if cluster_info.shared_contents:
|
|
260
|
+
print(" Contents:")
|
|
261
|
+
for item in cluster_info.shared_contents:
|
|
262
|
+
print(f" - {item}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _print_methods(methods):
|
|
266
|
+
"""Format and print method list."""
|
|
267
|
+
if not methods:
|
|
268
|
+
print("[INFO] No methods found")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
print("[INFO] Available Methods:")
|
|
272
|
+
for m in methods:
|
|
273
|
+
print(f" - {m.name} (Supports: {m.supported_data})")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _print_datasets(datasets):
|
|
277
|
+
"""Format and print dataset list."""
|
|
278
|
+
if not datasets:
|
|
279
|
+
print("[INFO] No datasets found")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
print("[INFO] Available Datasets:")
|
|
283
|
+
for d in datasets:
|
|
284
|
+
size_str = f" ({d.size_mb:.1f} MB, {d.file_count} files)" if d.size_mb > 0 else ""
|
|
285
|
+
print(f" - {d.name}{size_str}")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _print_experiments(experiments):
|
|
289
|
+
"""Format and print experiment list."""
|
|
290
|
+
if not experiments:
|
|
291
|
+
print("[INFO] No experiments found")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
print("[INFO] Recent Experiments:")
|
|
295
|
+
for e in experiments:
|
|
296
|
+
tags = f"[{e.status}]"
|
|
297
|
+
if e.has_results:
|
|
298
|
+
tags += " [results]"
|
|
299
|
+
if e.has_evaluation:
|
|
300
|
+
tags += " [eval]"
|
|
301
|
+
print(f" - {e.name} {tags}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _print_services(services):
|
|
305
|
+
"""Format and print running services."""
|
|
306
|
+
if not services:
|
|
307
|
+
print("\n[INFO] Running Services: None")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
print("\n[INFO] Running Services:")
|
|
311
|
+
print(f" {'NAME':<50} {'REPLICAS':<15} {'IMAGE':<30}")
|
|
312
|
+
print(" " + "-" * 95)
|
|
313
|
+
for svc in services:
|
|
314
|
+
name = svc['name'][:49]
|
|
315
|
+
replicas = str(svc.get('replicas', ''))[:14]
|
|
316
|
+
image = svc.get('image', '')[:29]
|
|
317
|
+
print(f" {name:<50} {replicas:<15} {image:<30}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
main()
|