BonanDing commited on
Commit
56f5ccc
·
1 Parent(s): bafe43d

Add DeMemWM multiview selector benchmark

Browse files
.exp_artifact/dememwm_dynamic_multiview_memory_selection_plan.md CHANGED
@@ -805,7 +805,7 @@ Use multiview dynamic memory online
805
 
806
  ## Substep 6: Add Selector Speed Benchmark
807
 
808
- Status: `[ ]`
809
 
810
  Goal:
811
 
 
805
 
806
  ## Substep 6: Add Selector Speed Benchmark
807
 
808
+ Status: `[x]`
809
 
810
  Goal:
811
 
scripts/benchmark_dememwm_multiview_selection.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark DeMemWM dynamic multiview memory selectors on synthetic poses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.util
7
+ import statistics
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ import torch
14
+
15
+
16
+ REPO_ROOT = Path(__file__).resolve().parents[1]
17
+ if str(REPO_ROOT) not in sys.path:
18
+ sys.path.insert(0, str(REPO_ROOT))
19
+
20
+ SELECTORS = ("fov_greedy", "pose_plucker_fps")
21
+
22
+
23
+ def _load_memory_selection_module():
24
+ module_path = REPO_ROOT / "datasets" / "video" / "memory_selection.py"
25
+ spec = importlib.util.spec_from_file_location("dememwm_memory_selection", module_path)
26
+ if spec is None or spec.loader is None:
27
+ raise ImportError(f"could not load memory selection module from {module_path}")
28
+ module = importlib.util.module_from_spec(spec)
29
+ sys.modules[spec.name] = module
30
+ spec.loader.exec_module(module)
31
+ return module
32
+
33
+
34
+ memory_selection = _load_memory_selection_module()
35
+
36
+
37
+ def _parse_args() -> argparse.Namespace:
38
+ parser = argparse.ArgumentParser(description=__doc__)
39
+ parser.add_argument("--num-frames", type=int, required=True)
40
+ parser.add_argument("--target-start", type=int, required=True)
41
+ parser.add_argument("--target-len", type=int, required=True)
42
+ parser.add_argument("--num-iters", type=int, required=True)
43
+ parser.add_argument("--pose-preselect-topk", type=int, required=True)
44
+ parser.add_argument("--candidate-chunk-size", type=int, required=True)
45
+ parser.add_argument("--selectors", nargs="+", default=list(SELECTORS))
46
+ parser.add_argument("--write-report", type=Path, default=None)
47
+ return parser.parse_args()
48
+
49
+
50
+ def _synthetic_poses(num_frames: int) -> np.ndarray:
51
+ frame = np.arange(num_frames, dtype=np.float32)
52
+ poses = np.zeros((num_frames, 5), dtype=np.float32)
53
+ poses[:, 0] = 0.03 * frame + 24.0 * np.sin(frame * 0.031)
54
+ poses[:, 1] = 4.0 * np.cos(frame * 0.019)
55
+ poses[:, 2] = 0.015 * frame + 24.0 * np.cos(frame * 0.027)
56
+ poses[:, 3] = 18.0 * np.sin(frame * 0.017)
57
+ poses[:, 4] = np.remainder(2.7 * frame + 30.0 * np.sin(frame * 0.011) + 180.0, 360.0) - 180.0
58
+ return poses
59
+
60
+
61
+ def _target_positions(target_start: int, target_len: int, num_frames: int) -> np.ndarray:
62
+ stop = target_start + target_len
63
+ if target_start < 0 or target_len <= 0 or stop > num_frames:
64
+ raise ValueError(
65
+ f"target window [{target_start}, {stop}) must be non-empty and inside num_frames={num_frames}"
66
+ )
67
+ return np.arange(target_start, stop, dtype=np.int64)
68
+
69
+
70
+ def _selection_cfg(selector: str, args: argparse.Namespace) -> dict:
71
+ return {
72
+ "enabled": True,
73
+ "causal": True,
74
+ "max_anchor_frames": 0,
75
+ "max_dynamic_frames": args.target_len,
76
+ "max_revisit_frames": 0,
77
+ "pose_similarity_threshold": 0.0,
78
+ "training_use_plucker": True,
79
+ "training_plucker_weight": 1.0,
80
+ "fov_overlap_threshold": 0.6,
81
+ "min_total_selected_coverage": 0.1,
82
+ "local_context_exclusion_frames": 8,
83
+ "plucker_moment_radius": 30.0,
84
+ "anchor_diverse_selection": True,
85
+ "pose_preselect_topk": args.pose_preselect_topk,
86
+ "candidate_chunk_size": args.candidate_chunk_size,
87
+ "dynamic": {
88
+ "selection_policy": "multiview",
89
+ "multiview_selector": selector,
90
+ },
91
+ }
92
+
93
+
94
+ def _percentile(values: list[float], fraction: float) -> float:
95
+ if not values:
96
+ return 0.0
97
+ ordered = sorted(values)
98
+ index = min(len(ordered) - 1, int(np.ceil(fraction * len(ordered))) - 1)
99
+ return ordered[index]
100
+
101
+
102
+ def _base_candidates(poses: np.ndarray, target_positions: np.ndarray, cfg: dict) -> np.ndarray:
103
+ return memory_selection._memory_candidate_frames(
104
+ len(poses),
105
+ target_positions,
106
+ cfg,
107
+ "training",
108
+ min_candidate_frame=0,
109
+ )
110
+
111
+
112
+ def _fov_candidate_count(poses: np.ndarray, target_positions: np.ndarray, cfg: dict) -> int:
113
+ candidates = _base_candidates(poses, target_positions, cfg)
114
+ poses_t = torch.as_tensor(poses, dtype=torch.float32)
115
+ preselected = memory_selection._pose_preselect(candidates, poses_t, target_positions, cfg)
116
+ return int(len(preselected))
117
+
118
+
119
+ def _pose_plucker_candidate_count(poses: np.ndarray, target_positions: np.ndarray, cfg: dict) -> int:
120
+ candidates = _base_candidates(poses, target_positions, cfg)
121
+ ranked_ids, _, _ = memory_selection._rank_pose_plucker_candidates(poses, candidates, target_positions, cfg)
122
+ topk = memory_selection.cfg_get(cfg, "pose_preselect_topk", 64)
123
+ if topk is not None and int(topk) > 0:
124
+ return int(min(int(topk), ranked_ids.numel()))
125
+ return int(ranked_ids.numel())
126
+
127
+
128
+ def _run_once(selector: str, poses: np.ndarray, target_positions: np.ndarray, cfg: dict, count: int) -> np.ndarray:
129
+ if selector == "fov_greedy":
130
+ candidates = _base_candidates(poses, target_positions, cfg)
131
+ pool = memory_selection._build_fov_candidate_pool(
132
+ poses,
133
+ candidates,
134
+ target_positions,
135
+ cfg,
136
+ use_plucker=True,
137
+ )
138
+ return memory_selection._select_dynamic_multiview(
139
+ poses,
140
+ target_positions,
141
+ cfg,
142
+ count,
143
+ split="training",
144
+ fov_pool=pool,
145
+ )
146
+ if selector == "pose_plucker_fps":
147
+ return memory_selection._select_dynamic_multiview(
148
+ poses,
149
+ target_positions,
150
+ cfg,
151
+ count,
152
+ split="training",
153
+ )
154
+ raise ValueError(f"unknown selector {selector!r}")
155
+
156
+
157
+ def _benchmark_selector(selector: str, poses: np.ndarray, target_positions: np.ndarray, args: argparse.Namespace) -> dict:
158
+ cfg = _selection_cfg(selector, args)
159
+ count = int(args.target_len)
160
+ if selector == "fov_greedy":
161
+ candidate_count = _fov_candidate_count(poses, target_positions, cfg)
162
+ fov_pool_reuse = True
163
+ else:
164
+ candidate_count = _pose_plucker_candidate_count(poses, target_positions, cfg)
165
+ fov_pool_reuse = False
166
+
167
+ selected = _run_once(selector, poses, target_positions, cfg, count)
168
+ timings_ms = []
169
+ for _ in range(args.num_iters):
170
+ start = time.perf_counter()
171
+ selected = _run_once(selector, poses, target_positions, cfg, count)
172
+ timings_ms.append((time.perf_counter() - start) * 1000.0)
173
+
174
+ return {
175
+ "selector": selector,
176
+ "mean_ms": statistics.fmean(timings_ms),
177
+ "median_ms": statistics.median(timings_ms),
178
+ "p90_ms": _percentile(timings_ms, 0.90),
179
+ "selected_count": int(len(selected)),
180
+ "candidate_count_after_pose_preselection": candidate_count,
181
+ "fov_pool_reuse": fov_pool_reuse,
182
+ "device": str(torch.device("cpu")),
183
+ }
184
+
185
+
186
+ def _format_results(results: list[dict]) -> str:
187
+ lines = [
188
+ "selector mean_ms median_ms p90_ms selected_count candidate_count_after_pose_preselection fov_pool_reuse device"
189
+ ]
190
+ for row in results:
191
+ lines.append(
192
+ "{selector} {mean_ms:.3f} {median_ms:.3f} {p90_ms:.3f} {selected_count} "
193
+ "{candidate_count_after_pose_preselection} {fov_pool_reuse} {device}".format(**row)
194
+ )
195
+ return "\n".join(lines)
196
+
197
+
198
+ def _write_report(path: Path, args: argparse.Namespace, results: list[dict]) -> None:
199
+ path.parent.mkdir(parents=True, exist_ok=True)
200
+ lines = [
201
+ "# DeMemWM Multiview Selection Speed Report",
202
+ "",
203
+ "This benchmark used deterministic synthetic poses only. It is not a substitute for a real dataset sampling benchmark.",
204
+ "",
205
+ "```text",
206
+ "python " + " ".join(sys.argv),
207
+ "```",
208
+ "",
209
+ "| selector | mean ms | median ms | p90 ms | selected frames | pose-preselected candidates | FOV pool reuse | device |",
210
+ "| --- | ---: | ---: | ---: | ---: | ---: | --- | --- |",
211
+ ]
212
+ for row in results:
213
+ lines.append(
214
+ "| {selector} | {mean_ms:.3f} | {median_ms:.3f} | {p90_ms:.3f} | {selected_count} | "
215
+ "{candidate_count_after_pose_preselection} | {fov_pool_reuse} | {device} |".format(**row)
216
+ )
217
+ lines.extend(
218
+ [
219
+ "",
220
+ f"Synthetic frames: {args.num_frames}",
221
+ f"Target window: [{args.target_start}, {args.target_start + args.target_len})",
222
+ f"Iterations: {args.num_iters}",
223
+ f"pose_preselect_topk: {args.pose_preselect_topk}",
224
+ f"candidate_chunk_size: {args.candidate_chunk_size}",
225
+ ]
226
+ )
227
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
228
+
229
+
230
+ def main() -> int:
231
+ args = _parse_args()
232
+ unknown = [selector for selector in args.selectors if selector not in SELECTORS]
233
+ if unknown:
234
+ valid = ", ".join(SELECTORS)
235
+ print(f"unknown selector(s): {', '.join(unknown)}; valid selectors: {valid}", file=sys.stderr)
236
+ return 2
237
+
238
+ poses = _synthetic_poses(args.num_frames)
239
+ target_positions = _target_positions(args.target_start, args.target_len, args.num_frames)
240
+ results = [_benchmark_selector(selector, poses, target_positions, args) for selector in args.selectors]
241
+ print(_format_results(results))
242
+
243
+ if args.write_report is not None:
244
+ _write_report(args.write_report, args, results)
245
+ print(f"wrote report: {args.write_report}")
246
+ return 0
247
+
248
+
249
+ if __name__ == "__main__":
250
+ raise SystemExit(main())
tests/test_dememwm_latent_dataset.py CHANGED
@@ -1,4 +1,6 @@
1
  import json
 
 
2
  import tempfile
3
  import unittest
4
  from unittest import mock
@@ -59,6 +61,10 @@ def _selection_cfg(**overrides):
59
  return OmegaConf.create(cfg)
60
 
61
 
 
 
 
 
62
  def _poses(num_frames):
63
  poses = np.zeros((num_frames, 5), dtype=np.float32)
64
  poses[:, 0] = np.arange(num_frames, dtype=np.float32)
@@ -1030,6 +1036,66 @@ class MemorySelectionTests(unittest.TestCase):
1030
  with self.assertRaisesRegex(ValueError, "fov_greedy, pose_plucker_fps"):
1031
  select_memory_indices(poses, np.array([6]), cfg)
1032
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1033
  def test_multiview_policy_selection_uses_shared_fov_pool_and_segments(self):
1034
  import datasets.video.memory_selection as memory_selection
1035
 
 
1
  import json
2
+ import subprocess
3
+ import sys
4
  import tempfile
5
  import unittest
6
  from unittest import mock
 
61
  return OmegaConf.create(cfg)
62
 
63
 
64
+ def _benchmark_script_path():
65
+ return Path(__file__).resolve().parents[1] / "scripts" / "benchmark_dememwm_multiview_selection.py"
66
+
67
+
68
  def _poses(num_frames):
69
  poses = np.zeros((num_frames, 5), dtype=np.float32)
70
  poses[:, 0] = np.arange(num_frames, dtype=np.float32)
 
1036
  with self.assertRaisesRegex(ValueError, "fov_greedy, pose_plucker_fps"):
1037
  select_memory_indices(poses, np.array([6]), cfg)
1038
 
1039
+ def test_benchmark_script_runs_synthetic_mode_without_dataset_paths(self):
1040
+ with tempfile.TemporaryDirectory() as tmpdir:
1041
+ report_path = Path(tmpdir) / "speed_report.md"
1042
+ result = subprocess.run(
1043
+ [
1044
+ sys.executable,
1045
+ str(_benchmark_script_path()),
1046
+ "--num-frames",
1047
+ "32",
1048
+ "--target-start",
1049
+ "16",
1050
+ "--target-len",
1051
+ "2",
1052
+ "--num-iters",
1053
+ "1",
1054
+ "--pose-preselect-topk",
1055
+ "4",
1056
+ "--candidate-chunk-size",
1057
+ "4",
1058
+ "--write-report",
1059
+ str(report_path),
1060
+ ],
1061
+ cwd=Path(__file__).resolve().parents[1],
1062
+ capture_output=True,
1063
+ text=True,
1064
+ )
1065
+
1066
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
1067
+ self.assertIn("fov_greedy", result.stdout)
1068
+ self.assertIn("pose_plucker_fps", result.stdout)
1069
+ self.assertIn("not a substitute for a real dataset sampling benchmark", report_path.read_text(encoding="utf-8"))
1070
+
1071
+ def test_benchmark_script_rejects_unknown_selector(self):
1072
+ result = subprocess.run(
1073
+ [
1074
+ sys.executable,
1075
+ str(_benchmark_script_path()),
1076
+ "--num-frames",
1077
+ "32",
1078
+ "--target-start",
1079
+ "16",
1080
+ "--target-len",
1081
+ "2",
1082
+ "--num-iters",
1083
+ "1",
1084
+ "--pose-preselect-topk",
1085
+ "4",
1086
+ "--candidate-chunk-size",
1087
+ "4",
1088
+ "--selectors",
1089
+ "fov_fps",
1090
+ ],
1091
+ cwd=Path(__file__).resolve().parents[1],
1092
+ capture_output=True,
1093
+ text=True,
1094
+ )
1095
+
1096
+ self.assertNotEqual(result.returncode, 0)
1097
+ self.assertIn("unknown selector", result.stderr)
1098
+
1099
  def test_multiview_policy_selection_uses_shared_fov_pool_and_segments(self):
1100
  import datasets.video.memory_selection as memory_selection
1101