vlalab 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.
@@ -0,0 +1,256 @@
1
+ """
2
+ VLA-Lab Latency Analysis Viewer
3
+
4
+ Deep dive into timing metrics for VLA deployment.
5
+ """
6
+
7
+ import streamlit as st
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ from pathlib import Path
11
+ import json
12
+ from typing import List, Dict, Any
13
+
14
+ import vlalab
15
+
16
+ # Setup matplotlib fonts
17
+ try:
18
+ from vlalab.viz.mpl_fonts import setup_matplotlib_fonts
19
+ setup_matplotlib_fonts(verbose=False)
20
+ except Exception:
21
+ pass
22
+
23
+
24
+ def load_timing_data(run_path: Path) -> List[Dict[str, Any]]:
25
+ """Load timing data from a run."""
26
+ timing_data = []
27
+ steps_path = run_path / "steps.jsonl"
28
+
29
+ if steps_path.exists():
30
+ with open(steps_path, "r") as f:
31
+ for line in f:
32
+ if line.strip():
33
+ step = json.loads(line)
34
+ timing_data.append(step.get("timing", {}))
35
+
36
+ return timing_data
37
+
38
+
39
+ def get_latency_ms(timing_dict: Dict, key_base: str) -> float:
40
+ """Get latency value in ms."""
41
+ new_key = f"{key_base}_ms"
42
+ if new_key in timing_dict and timing_dict[new_key] is not None:
43
+ return timing_dict[new_key]
44
+ if key_base in timing_dict and timing_dict[key_base] is not None:
45
+ return timing_dict[key_base] * 1000
46
+ return np.nan
47
+
48
+
49
+ def render():
50
+ """Render the latency analysis page."""
51
+ st.title("📈 时延深度分析")
52
+
53
+ # Sidebar: show current runs directory
54
+ runs_dir = vlalab.get_runs_dir()
55
+ st.sidebar.markdown("### 日志目录")
56
+ st.sidebar.code(str(runs_dir))
57
+
58
+ # List projects
59
+ projects = vlalab.list_projects()
60
+
61
+ if not projects:
62
+ st.info(f"未找到任何项目。日志目录: `{runs_dir}`")
63
+ return
64
+
65
+ # Project filter
66
+ selected_project = st.sidebar.selectbox(
67
+ "选择项目",
68
+ ["全部"] + projects,
69
+ )
70
+
71
+ # List runs
72
+ if selected_project == "全部":
73
+ run_paths = vlalab.list_runs()
74
+ else:
75
+ run_paths = vlalab.list_runs(project=selected_project)
76
+
77
+ if not run_paths:
78
+ st.info("该项目下没有运行记录。")
79
+ return
80
+
81
+ # Multi-select for comparison
82
+ selected_runs = st.sidebar.multiselect(
83
+ "选择运行 (可多选比较)",
84
+ run_paths,
85
+ default=[run_paths[0]] if run_paths else [],
86
+ format_func=lambda p: f"{p.name} ({p.parent.name})"
87
+ )
88
+
89
+ if not selected_runs:
90
+ st.info("请选择至少一个运行")
91
+ return
92
+
93
+ # Load data
94
+ all_timing_data = {}
95
+ for run_path in selected_runs:
96
+ timing_data = load_timing_data(run_path)
97
+ if timing_data:
98
+ all_timing_data[run_path.name] = timing_data
99
+
100
+ if not all_timing_data:
101
+ st.warning("选中的运行没有时延数据")
102
+ return
103
+
104
+ # Analysis tabs
105
+ tab1, tab2, tab3 = st.tabs(["📊 时序图", "📈 统计分布", "🔍 详细对比"])
106
+
107
+ with tab1:
108
+ st.markdown("### 时延时序图")
109
+
110
+ fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
111
+
112
+ colors = plt.cm.tab10(np.linspace(0, 1, len(all_timing_data)))
113
+
114
+ for (run_name, timing_list), color in zip(all_timing_data.items(), colors):
115
+ steps = range(len(timing_list))
116
+
117
+ trans_lats = [get_latency_ms(t, "transport_latency") for t in timing_list]
118
+ infer_lats = [get_latency_ms(t, "inference_latency") for t in timing_list]
119
+ total_lats = [get_latency_ms(t, "total_latency") for t in timing_list]
120
+
121
+ axes[0].plot(steps, trans_lats, color=color, alpha=0.7, label=run_name)
122
+ axes[1].plot(steps, infer_lats, color=color, alpha=0.7, label=run_name)
123
+ axes[2].plot(steps, total_lats, color=color, alpha=0.7, label=run_name)
124
+
125
+ axes[0].set_ylabel("Transport (ms)")
126
+ axes[0].set_title("传输延迟 (网络)")
127
+ axes[0].legend(loc='upper right')
128
+ axes[0].grid(True, alpha=0.3)
129
+ axes[0].axhline(50, color='orange', linestyle='--', alpha=0.5)
130
+
131
+ axes[1].set_ylabel("Inference (ms)")
132
+ axes[1].set_title("推理延迟 (GPU)")
133
+ axes[1].legend(loc='upper right')
134
+ axes[1].grid(True, alpha=0.3)
135
+
136
+ axes[2].set_ylabel("Total (ms)")
137
+ axes[2].set_xlabel("Step")
138
+ axes[2].set_title("总回路延迟")
139
+ axes[2].legend(loc='upper right')
140
+ axes[2].grid(True, alpha=0.3)
141
+ axes[2].axhline(100, color='red', linestyle='--', alpha=0.5)
142
+
143
+ plt.tight_layout()
144
+ st.pyplot(fig)
145
+ plt.close(fig)
146
+
147
+ with tab2:
148
+ st.markdown("### 延迟分布")
149
+
150
+ fig, axes = plt.subplots(1, 3, figsize=(14, 4))
151
+
152
+ for run_name, timing_list in all_timing_data.items():
153
+ trans_lats = [get_latency_ms(t, "transport_latency") for t in timing_list]
154
+ infer_lats = [get_latency_ms(t, "inference_latency") for t in timing_list]
155
+ total_lats = [get_latency_ms(t, "total_latency") for t in timing_list]
156
+
157
+ trans_valid = [x for x in trans_lats if not np.isnan(x)]
158
+ infer_valid = [x for x in infer_lats if not np.isnan(x)]
159
+ total_valid = [x for x in total_lats if not np.isnan(x)]
160
+
161
+ if trans_valid:
162
+ axes[0].hist(trans_valid, bins=30, alpha=0.5, label=run_name)
163
+ if infer_valid:
164
+ axes[1].hist(infer_valid, bins=30, alpha=0.5, label=run_name)
165
+ if total_valid:
166
+ axes[2].hist(total_valid, bins=30, alpha=0.5, label=run_name)
167
+
168
+ axes[0].set_xlabel("Transport Latency (ms)")
169
+ axes[0].set_ylabel("Count")
170
+ axes[0].set_title("传输延迟分布")
171
+ axes[0].legend()
172
+
173
+ axes[1].set_xlabel("Inference Latency (ms)")
174
+ axes[1].set_ylabel("Count")
175
+ axes[1].set_title("推理延迟分布")
176
+ axes[1].legend()
177
+
178
+ axes[2].set_xlabel("Total Latency (ms)")
179
+ axes[2].set_ylabel("Count")
180
+ axes[2].set_title("总延迟分布")
181
+ axes[2].legend()
182
+
183
+ plt.tight_layout()
184
+ st.pyplot(fig)
185
+ plt.close(fig)
186
+
187
+ with tab3:
188
+ st.markdown("### 详细统计对比")
189
+
190
+ # Create comparison table
191
+ stats_data = []
192
+
193
+ for run_name, timing_list in all_timing_data.items():
194
+ trans_lats = [get_latency_ms(t, "transport_latency") for t in timing_list]
195
+ infer_lats = [get_latency_ms(t, "inference_latency") for t in timing_list]
196
+ total_lats = [get_latency_ms(t, "total_latency") for t in timing_list]
197
+
198
+ trans_valid = [x for x in trans_lats if not np.isnan(x)]
199
+ infer_valid = [x for x in infer_lats if not np.isnan(x)]
200
+ total_valid = [x for x in total_lats if not np.isnan(x)]
201
+
202
+ stats = {
203
+ "运行": run_name,
204
+ "步数": len(timing_list),
205
+ }
206
+
207
+ if trans_valid:
208
+ stats["传输-平均(ms)"] = f"{np.mean(trans_valid):.1f}"
209
+ stats["传输-P95(ms)"] = f"{np.percentile(trans_valid, 95):.1f}"
210
+
211
+ if infer_valid:
212
+ stats["推理-平均(ms)"] = f"{np.mean(infer_valid):.1f}"
213
+ stats["推理-P95(ms)"] = f"{np.percentile(infer_valid, 95):.1f}"
214
+
215
+ if total_valid:
216
+ stats["总计-平均(ms)"] = f"{np.mean(total_valid):.1f}"
217
+ stats["总计-P95(ms)"] = f"{np.percentile(total_valid, 95):.1f}"
218
+ stats["总计-最大(ms)"] = f"{np.max(total_valid):.1f}"
219
+
220
+ stats_data.append(stats)
221
+
222
+ import pandas as pd
223
+ df = pd.DataFrame(stats_data)
224
+ st.dataframe(df, use_container_width=True)
225
+
226
+ # Performance assessment
227
+ st.markdown("### 性能评估")
228
+
229
+ for run_name, timing_list in all_timing_data.items():
230
+ total_lats = [get_latency_ms(t, "total_latency") for t in timing_list]
231
+ total_valid = [x for x in total_lats if not np.isnan(x)]
232
+
233
+ if total_valid:
234
+ avg = np.mean(total_valid)
235
+ p95 = np.percentile(total_valid, 95)
236
+
237
+ col1, col2, col3 = st.columns(3)
238
+
239
+ with col1:
240
+ st.markdown(f"**{run_name}**")
241
+
242
+ with col2:
243
+ if avg < 50:
244
+ st.success(f"平均延迟: {avg:.1f}ms ✓")
245
+ elif avg < 100:
246
+ st.warning(f"平均延迟: {avg:.1f}ms")
247
+ else:
248
+ st.error(f"平均延迟: {avg:.1f}ms ✗")
249
+
250
+ with col3:
251
+ if p95 < 100:
252
+ st.success(f"P95延迟: {p95:.1f}ms ✓")
253
+ elif p95 < 200:
254
+ st.warning(f"P95延迟: {p95:.1f}ms")
255
+ else:
256
+ st.error(f"P95延迟: {p95:.1f}ms ✗")
vlalab/cli.py ADDED
@@ -0,0 +1,137 @@
1
+ """
2
+ VLA-Lab Command Line Interface
3
+
4
+ Commands:
5
+ - vlalab view: Launch Streamlit visualization app
6
+ - vlalab convert: Convert old log formats to VLA-Lab run format
7
+ - vlalab init-run: Initialize a new run directory
8
+ - vlalab info: Show information about a run
9
+ """
10
+
11
+ import click
12
+ from pathlib import Path
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ console = Console()
17
+
18
+
19
+ @click.group()
20
+ @click.version_option()
21
+ def main():
22
+ """VLA-Lab: Track and visualize VLA real-world deployment."""
23
+ pass
24
+
25
+
26
+ @main.command()
27
+ @click.option(
28
+ "--port", "-p", default=8501, type=int, help="Port for Streamlit server"
29
+ )
30
+ @click.option(
31
+ "--run-dir", "-r", default=None, type=click.Path(exists=True),
32
+ help="Default run directory to load"
33
+ )
34
+ def view(port: int, run_dir: str):
35
+ """Launch the Streamlit visualization app."""
36
+ import subprocess
37
+ import sys
38
+
39
+ app_path = Path(__file__).parent / "apps" / "streamlit" / "app.py"
40
+
41
+ if not app_path.exists():
42
+ # Fallback to package data location
43
+ import vlalab
44
+ package_dir = Path(vlalab.__file__).parent
45
+ app_path = package_dir / "apps" / "streamlit" / "app.py"
46
+
47
+ cmd = [sys.executable, "-m", "streamlit", "run", str(app_path), "--server.port", str(port)]
48
+
49
+ if run_dir:
50
+ cmd.extend(["--", "--run-dir", run_dir])
51
+
52
+ console.print(f"[green]Starting VLA-Lab viewer on port {port}...[/green]")
53
+ subprocess.run(cmd)
54
+
55
+
56
+ @main.command()
57
+ @click.argument("input_path", type=click.Path(exists=True))
58
+ @click.option(
59
+ "--output", "-o", default=None, type=click.Path(),
60
+ help="Output run directory (default: auto-generated)"
61
+ )
62
+ @click.option(
63
+ "--format", "-f", "input_format",
64
+ type=click.Choice(["dp", "groot", "auto"]),
65
+ default="auto",
66
+ help="Input log format"
67
+ )
68
+ def convert(input_path: str, output: str, input_format: str):
69
+ """Convert old log formats to VLA-Lab run format."""
70
+ from vlalab.adapters.converter import convert_legacy_log
71
+
72
+ input_path = Path(input_path)
73
+
74
+ if output is None:
75
+ output = input_path.parent / f"run_{input_path.stem}"
76
+
77
+ output = Path(output)
78
+
79
+ console.print(f"[blue]Converting {input_path} -> {output}[/blue]")
80
+
81
+ try:
82
+ stats = convert_legacy_log(input_path, output, input_format)
83
+ console.print(f"[green]Converted {stats['steps']} steps, {stats['images']} images[/green]")
84
+ except Exception as e:
85
+ console.print(f"[red]Error: {e}[/red]")
86
+ raise click.Abort()
87
+
88
+
89
+ @main.command("init-run")
90
+ @click.argument("run_dir", type=click.Path())
91
+ @click.option("--model", "-m", default="unknown", help="Model name/path")
92
+ @click.option("--task", "-t", default="unknown", help="Task name")
93
+ @click.option("--robot", "-r", default="unknown", help="Robot name")
94
+ def init_run(run_dir: str, model: str, task: str, robot: str):
95
+ """Initialize a new run directory with metadata."""
96
+ from vlalab.logging import RunLogger
97
+
98
+ run_dir = Path(run_dir)
99
+
100
+ logger = RunLogger(
101
+ run_dir=run_dir,
102
+ model_name=model,
103
+ task_name=task,
104
+ robot_name=robot,
105
+ )
106
+
107
+ console.print(f"[green]Initialized run directory: {run_dir}[/green]")
108
+ console.print(f" Model: {model}")
109
+ console.print(f" Task: {task}")
110
+ console.print(f" Robot: {robot}")
111
+
112
+
113
+ @main.command()
114
+ @click.argument("run_dir", type=click.Path(exists=True))
115
+ def info(run_dir: str):
116
+ """Show information about a run."""
117
+ from vlalab.logging.run_loader import load_run_info
118
+
119
+ run_dir = Path(run_dir)
120
+
121
+ try:
122
+ info = load_run_info(run_dir)
123
+
124
+ table = Table(title=f"Run: {run_dir.name}")
125
+ table.add_column("Field", style="cyan")
126
+ table.add_column("Value", style="green")
127
+
128
+ for key, value in info.items():
129
+ table.add_row(key, str(value))
130
+
131
+ console.print(table)
132
+ except Exception as e:
133
+ console.print(f"[red]Error loading run: {e}[/red]")
134
+
135
+
136
+ if __name__ == "__main__":
137
+ main()