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.
- vlalab/__init__.py +82 -0
- vlalab/adapters/__init__.py +10 -0
- vlalab/adapters/converter.py +146 -0
- vlalab/adapters/dp_adapter.py +181 -0
- vlalab/adapters/groot_adapter.py +148 -0
- vlalab/apps/__init__.py +1 -0
- vlalab/apps/streamlit/__init__.py +1 -0
- vlalab/apps/streamlit/app.py +103 -0
- vlalab/apps/streamlit/pages/__init__.py +1 -0
- vlalab/apps/streamlit/pages/dataset_viewer.py +322 -0
- vlalab/apps/streamlit/pages/inference_viewer.py +360 -0
- vlalab/apps/streamlit/pages/latency_viewer.py +256 -0
- vlalab/cli.py +137 -0
- vlalab/core.py +672 -0
- vlalab/logging/__init__.py +10 -0
- vlalab/logging/jsonl_writer.py +114 -0
- vlalab/logging/run_loader.py +216 -0
- vlalab/logging/run_logger.py +343 -0
- vlalab/schema/__init__.py +17 -0
- vlalab/schema/run.py +162 -0
- vlalab/schema/step.py +177 -0
- vlalab/viz/__init__.py +9 -0
- vlalab/viz/mpl_fonts.py +161 -0
- vlalab-0.1.0.dist-info/METADATA +443 -0
- vlalab-0.1.0.dist-info/RECORD +29 -0
- vlalab-0.1.0.dist-info/WHEEL +5 -0
- vlalab-0.1.0.dist-info/entry_points.txt +2 -0
- vlalab-0.1.0.dist-info/licenses/LICENSE +21 -0
- vlalab-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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()
|