BonanDing commited on
Commit
927c275
·
1 Parent(s): 310850d

Implement DeMemWM online validation memory

Browse files
algorithms/dememwm/df_video.py CHANGED
@@ -17,6 +17,7 @@ from lightning.pytorch.utilities.types import STEP_OUTPUT
17
  from algorithms.common.metrics import (
18
  LearnedPerceptualImagePatchSimilarity,
19
  )
 
20
  from utils.logging_utils import log_video, get_validation_metrics_for_videos
21
  from .df_base import DiffusionForcingBase
22
  from .models.vae import VAE_models
@@ -151,6 +152,19 @@ def _preprocess_dememwm_latent_batch(batch):
151
  }
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def euler_to_rotation_matrix(pitch, yaw):
155
  """
156
  Convert pitch and yaw angles (in radians) to a 3x3 rotation matrix.
@@ -904,76 +918,114 @@ class DeMemWMMinecraft(DiffusionForcingBase):
904
  memory_condition_length = self.memory_condition_length
905
  preprocessed = self._preprocess_batch(batch)
906
  if isinstance(preprocessed, Mapping):
907
- xs = preprocessed["latents"]
 
908
  target_length = preprocessed["target_length"]
909
  batch_size = xs.shape[1]
910
- conditions = preprocessed["action_conditions"].to(device=xs.device)
911
- frame_indices = preprocessed["frame_indices"].to(device=xs.device)
912
- frame_memory_pose = preprocessed["frame_memory_pose"].to(device=xs.device, dtype=xs.dtype)
913
  image_hw = preprocessed["image_hw"].to(device=xs.device)
914
- frame_memory_masks = {
915
- key: mask.to(device=xs.device)
916
- for key, mask in preprocessed["memory_masks"].items()
917
- }
 
 
 
918
 
919
  xs_pred = xs.clone()
920
- # Single-window latent validation: only target frames are sampled;
921
- # appended memory latents stay exactly as provided by the dataset.
922
- xs_pred[:target_length] = torch.randn_like(xs_pred[:target_length]).clamp(
923
- -self.clip_noise,
924
- self.clip_noise,
925
- )
926
- scheduling_matrix = self._generate_scheduling_matrix(target_length)
927
- memory_length = xs.shape[0] - target_length
928
- memory_noise_levels = torch.zeros(
929
- (memory_length, batch_size),
930
- device=xs.device,
931
- dtype=torch.long,
932
- )
933
 
934
- for m in range(scheduling_matrix.shape[0] - 1):
935
- from_target = torch.as_tensor(
936
- scheduling_matrix[m],
937
- device=xs.device,
938
- dtype=torch.long,
939
- )[:, None].repeat(1, batch_size)
940
- to_target = torch.as_tensor(
941
- scheduling_matrix[m + 1],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
  device=xs.device,
943
  dtype=torch.long,
944
- )[:, None].repeat(1, batch_size)
945
- from_noise_levels = torch.cat([from_target, memory_noise_levels], dim=0)
946
- to_noise_levels = torch.cat([to_target, memory_noise_levels], dim=0)
947
-
948
- sampled_target = self.diffusion_model.sample_step(
949
- xs_pred,
950
- conditions,
951
- None,
952
- from_noise_levels,
953
- to_noise_levels,
954
- current_frame=0,
955
- mode=namespace,
956
- reference_length=0,
957
- frame_idx=frame_indices,
958
- frame_memory_segments=preprocessed["memory_segments"],
959
- frame_memory_masks=frame_memory_masks,
960
- frame_memory_pose=frame_memory_pose,
961
- image_hw=image_hw,
962
  )
963
- xs_pred[:target_length] = sampled_target[:target_length]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
 
965
- latent_loss = F.mse_loss(
966
- xs_pred[:target_length],
967
- xs[:target_length],
968
- reduction="none",
969
- )
970
- target_mask = frame_memory_masks.get("target")
971
  if target_mask is not None:
972
- target_mask = rearrange(target_mask.to(dtype=latent_loss.dtype), "b t -> t b")
973
- target_mask = target_mask.view(*target_mask.shape, *((1,) * (latent_loss.ndim - 2)))
974
- latent_loss = (latent_loss * target_mask).sum() / target_mask.expand_as(latent_loss).sum().clamp_min(1.0)
975
- else:
976
  latent_loss = latent_loss.mean()
 
 
977
 
978
  self.log(f"{namespace}/latent_mse", latent_loss.detach())
979
  return latent_loss
 
17
  from algorithms.common.metrics import (
18
  LearnedPerceptualImagePatchSimilarity,
19
  )
20
+ from datasets.video.memory_selection import select_memory_indices
21
  from utils.logging_utils import log_video, get_validation_metrics_for_videos
22
  from .df_base import DiffusionForcingBase
23
  from .models.vae import VAE_models
 
152
  }
153
 
154
 
155
+ def _gather_online_memory_tensor(source, indices, masks):
156
+ output_shape = (indices.shape[1], indices.shape[0], *source.shape[2:])
157
+ if output_shape[0] == 0 or source.shape[0] == 0:
158
+ return source.new_zeros(output_shape)
159
+
160
+ gather_idx = indices.clamp(0, source.shape[0] - 1).T.to(source.device)
161
+ batch_idx = torch.arange(indices.shape[0], device=source.device).expand_as(gather_idx)
162
+ gathered = source[gather_idx, batch_idx]
163
+ mask = masks.T.to(device=source.device, dtype=torch.bool)
164
+ mask = mask.view(*mask.shape, *((1,) * (gathered.ndim - 2)))
165
+ return gathered * mask.to(dtype=gathered.dtype)
166
+
167
+
168
  def euler_to_rotation_matrix(pitch, yaw):
169
  """
170
  Convert pitch and yaw angles (in radians) to a 3x3 rotation matrix.
 
918
  memory_condition_length = self.memory_condition_length
919
  preprocessed = self._preprocess_batch(batch)
920
  if isinstance(preprocessed, Mapping):
921
+ target_tensors = preprocessed["target_tensors"]
922
+ xs = target_tensors["latents"]
923
  target_length = preprocessed["target_length"]
924
  batch_size = xs.shape[1]
 
 
 
925
  image_hw = preprocessed["image_hw"].to(device=xs.device)
926
+ target_mask = preprocessed["memory_masks"].get("target")
927
+ target_mask = None if target_mask is None else target_mask.to(device=xs.device)
928
+ target_poses = target_tensors["poses"].to(device=xs.device, dtype=xs.dtype)
929
+ target_frame_indices = target_tensors["frame_indices"].to(device=xs.device)
930
+ stream_lengths = preprocessed["stream_lengths"]
931
+ memory_selection_cfg = _cfg_get(self.cfg, "memory_selection", {})
932
+ poses_np = target_poses.detach().cpu().numpy()
933
 
934
  xs_pred = xs.clone()
935
+ n_context_frames = min(self.context_frames // self.frame_stack, target_length)
936
+ curr_frame = n_context_frames
 
 
 
 
 
 
 
 
 
 
 
937
 
938
+ while curr_frame < target_length:
939
+ horizon = min(target_length - curr_frame, self.chunk_size) if self.chunk_size > 0 else target_length - curr_frame
940
+ target_slice = slice(curr_frame, curr_frame + horizon)
941
+ xs_pred[target_slice] = torch.randn_like(xs_pred[target_slice]).clamp(-self.clip_noise, self.clip_noise)
942
+
943
+ stream_indices = {
944
+ key: torch.full((batch_size, int(stream_lengths[key])), -1, device=xs.device, dtype=torch.long)
945
+ for key in _DEMEMWM_STREAM_KEYS
946
+ }
947
+ stream_masks = {key: torch.zeros_like(indices, dtype=torch.bool) for key, indices in stream_indices.items()}
948
+ target_positions = np.arange(target_slice.start, target_slice.stop, dtype=np.int64)
949
+ for batch_i in range(batch_size):
950
+ selected, selected_masks = select_memory_indices(
951
+ poses_np[:, batch_i, :5],
952
+ target_positions,
953
+ memory_selection_cfg,
954
+ split=namespace,
955
+ )
956
+ for key, indices in stream_indices.items():
957
+ count = indices.shape[1]
958
+ selected_key = np.asarray(selected.get(key, []), dtype=np.int64)[:count]
959
+ mask_key = np.asarray(selected_masks.get(key, []), dtype=bool)[:count]
960
+ indices[batch_i, : len(selected_key)] = torch.as_tensor(selected_key, device=xs.device, dtype=torch.long)
961
+ stream_masks[key][batch_i, : len(mask_key)] = torch.as_tensor(mask_key, device=xs.device, dtype=torch.bool)
962
+
963
+ source_latents = xs_pred[:curr_frame]
964
+ source_poses = target_poses[:curr_frame]
965
+ source_frame_indices = target_frame_indices[:curr_frame]
966
+ frame_memory_masks = {
967
+ "target": torch.ones((batch_size, horizon), device=xs.device, dtype=torch.bool)
968
+ if target_mask is None
969
+ else target_mask[:, target_slice],
970
+ **stream_masks,
971
+ }
972
+ stream_latents = [_gather_online_memory_tensor(source_latents, stream_indices[key], stream_masks[key]) for key in _DEMEMWM_STREAM_KEYS]
973
+ stream_poses = [_gather_online_memory_tensor(source_poses, stream_indices[key], stream_masks[key]) for key in _DEMEMWM_STREAM_KEYS]
974
+ stream_frame_indices = [_gather_online_memory_tensor(source_frame_indices, stream_indices[key], stream_masks[key]) for key in _DEMEMWM_STREAM_KEYS]
975
+ target_conditions = target_tensors["action_conditions"][target_slice].to(device=xs.device)
976
+ memory_length = sum(mask.shape[1] for mask in stream_masks.values())
977
+
978
+ packed_latents = torch.cat([xs_pred[target_slice], *stream_latents], dim=0)
979
+ packed_conditions = torch.cat(
980
+ [target_conditions, target_conditions.new_zeros((memory_length, batch_size, target_conditions.shape[-1]))],
981
+ dim=0,
982
+ )
983
+ frame_memory_pose = torch.cat([target_poses[target_slice], *stream_poses], dim=0)
984
+ frame_indices = torch.cat([target_frame_indices[target_slice], *stream_frame_indices], dim=0)
985
+ frame_memory_segments = {
986
+ "target": int(horizon),
987
+ **{key: int(stream_masks[key].shape[1]) for key in _DEMEMWM_STREAM_KEYS},
988
+ }
989
+ memory_noise_levels = torch.zeros(
990
+ (packed_latents.shape[0] - horizon, batch_size),
991
  device=xs.device,
992
  dtype=torch.long,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
  )
994
+ scheduling_matrix = self._generate_scheduling_matrix(horizon)
995
+
996
+ for m in range(scheduling_matrix.shape[0] - 1):
997
+ from_target = torch.as_tensor(scheduling_matrix[m], device=xs.device, dtype=torch.long)[:, None].repeat(1, batch_size)
998
+ to_target = torch.as_tensor(scheduling_matrix[m + 1], device=xs.device, dtype=torch.long)[:, None].repeat(1, batch_size)
999
+ sampled_target = self.diffusion_model.sample_step(
1000
+ packed_latents,
1001
+ packed_conditions,
1002
+ None,
1003
+ torch.cat([from_target, memory_noise_levels], dim=0),
1004
+ torch.cat([to_target, memory_noise_levels], dim=0),
1005
+ current_frame=curr_frame,
1006
+ mode=namespace,
1007
+ reference_length=0,
1008
+ frame_idx=frame_indices,
1009
+ frame_memory_segments=frame_memory_segments,
1010
+ frame_memory_masks=frame_memory_masks,
1011
+ frame_memory_pose=frame_memory_pose,
1012
+ image_hw=image_hw,
1013
+ )
1014
+ packed_latents[:horizon] = sampled_target[:horizon]
1015
 
1016
+ xs_pred[target_slice] = packed_latents[:horizon]
1017
+ curr_frame += horizon
1018
+
1019
+ eval_start = n_context_frames
1020
+ latent_loss = F.mse_loss(xs_pred[eval_start:target_length], xs[eval_start:target_length], reduction="none")
 
1021
  if target_mask is not None:
1022
+ loss_mask = rearrange(target_mask[:, eval_start:target_length].to(dtype=latent_loss.dtype), "b t -> t b")
1023
+ loss_mask = loss_mask.view(*loss_mask.shape, *((1,) * (latent_loss.ndim - 2)))
1024
+ latent_loss = (latent_loss * loss_mask).sum() / loss_mask.expand_as(latent_loss).sum().clamp_min(1.0)
1025
+ elif latent_loss.numel():
1026
  latent_loss = latent_loss.mean()
1027
+ else:
1028
+ latent_loss = xs_pred.new_tensor(0.0)
1029
 
1030
  self.log(f"{namespace}/latent_mse", latent_loss.detach())
1031
  return latent_loss
tests/test_dememwm_latent_dataset.py CHANGED
@@ -1,5 +1,6 @@
1
  import tempfile
2
  import unittest
 
3
  from pathlib import Path
4
 
5
  import numpy as np
@@ -300,9 +301,17 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
300
  self.assertEqual(harness.logged[0][0], "training/loss")
301
  self.assertEqual(float(harness.logged[0][1]), 2.0)
302
 
303
- def test_validation_step_uses_latent_dict_target_sampling_and_metric(self):
 
304
  from algorithms.dememwm.df_video import DeMemWMMinecraft, _preprocess_dememwm_latent_batch
305
 
 
 
 
 
 
 
 
306
  class FakeDiffusion:
307
  def __init__(self):
308
  self.calls = []
@@ -318,16 +327,15 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
318
  "kwargs": kwargs,
319
  }
320
  )
321
- target_length = kwargs["frame_memory_segments"]["target"]
322
- sampled = x.clone()
323
- sampled[:target_length] = float(len(self.calls))
324
- sampled[target_length:] = 999.0
325
- return sampled
326
 
327
  class Harness:
328
  def __init__(self):
329
- self.memory_condition_length = 3
330
- self.clip_noise = 0.0
 
 
 
331
  self.diffusion_model = FakeDiffusion()
332
  self.logged = []
333
  self.horizons = []
@@ -337,10 +345,10 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
337
 
338
  def _generate_scheduling_matrix(self, horizon):
339
  self.horizons.append(horizon)
340
- return np.array([[2, 2], [1, 1], [0, 0]], dtype=np.int64)
341
 
342
  def _generate_condition_indices(self, *args, **kwargs):
343
- raise AssertionError("latent dict validation must not reselect memory")
344
 
345
  def encode(self, xs):
346
  raise AssertionError("latent dict validation must not VAE-encode inputs")
@@ -352,51 +360,33 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
352
  self.logged.append((name, value))
353
 
354
  batch = {
355
- "latents": torch.arange(5, dtype=torch.float32).view(1, 5, 1, 1, 1),
356
- "actions": (100 + torch.arange(5, dtype=torch.float32)).view(1, 5, 1),
357
- "poses": (200 + torch.arange(5, dtype=torch.float32)).view(1, 5, 1).repeat(1, 1, 5),
358
- "frame_indices": (10 + torch.arange(5, dtype=torch.long)).view(1, 5),
359
- "memory_segments": {
360
- "target": torch.tensor([2]),
361
- "anchor": torch.tensor([1]),
362
- "dynamic": torch.tensor([1]),
363
- "revisit": torch.tensor([1]),
364
- },
365
- "memory_masks": {
366
- "target": torch.tensor([[True, False]]),
367
- "anchor": torch.tensor([[True]]),
368
- "dynamic": torch.tensor([[False]]),
369
- "revisit": torch.tensor([[True]]),
370
- },
371
  "image_hw": torch.tensor([[360, 640]], dtype=torch.long),
372
  }
373
  harness = Harness()
374
 
375
- loss = DeMemWMMinecraft.validation_step(harness, batch, 0, namespace="test")
 
376
  calls = harness.diffusion_model.calls
 
377
 
378
- self.assertEqual(harness.horizons, [2])
379
- self.assertEqual(len(calls), 2)
380
- for call in calls:
381
- self.assertEqual(tuple(call["x"].shape), (5, 1, 1, 1, 1))
382
- self.assertEqual(call["x"][2:, 0, 0, 0, 0].tolist(), [2.0, 3.0, 4.0])
383
- self.assertEqual(call["action_cond"][:, 0, 0].tolist(), [0.0, 101.0, 0.0, 0.0, 0.0])
384
- self.assertIsNone(call["pose_cond"])
385
- self.assertEqual(call["kwargs"]["reference_length"], 0)
386
- self.assertEqual(call["kwargs"]["current_frame"], 0)
387
- self.assertEqual(call["kwargs"]["mode"], "test")
388
- self.assertEqual(call["kwargs"]["frame_idx"][:, 0].tolist(), [10, 11, 12, 13, 14])
389
- self.assertEqual(call["kwargs"]["frame_memory_segments"], {"target": 2, "anchor": 1, "dynamic": 1, "revisit": 1})
390
- self.assertEqual(call["kwargs"]["frame_memory_pose"][:, 0, 0].tolist(), [200.0, 201.0, 202.0, 203.0, 204.0])
391
- self.assertEqual(call["kwargs"]["frame_memory_masks"]["dynamic"].tolist(), [[False]])
392
- self.assertEqual(call["kwargs"]["image_hw"].tolist(), [[360, 640]])
393
- self.assertEqual(calls[0]["curr_noise_level"][:, 0].tolist(), [2, 2, 0, 0, 0])
394
- self.assertEqual(calls[0]["next_noise_level"][:, 0].tolist(), [1, 1, 0, 0, 0])
395
- self.assertEqual(calls[1]["curr_noise_level"][:, 0].tolist(), [1, 1, 0, 0, 0])
396
- self.assertEqual(calls[1]["next_noise_level"][:, 0].tolist(), [0, 0, 0, 0, 0])
397
- self.assertEqual(float(loss), 4.0)
398
- self.assertEqual(harness.logged[0][0], "test/latent_mse")
399
- self.assertEqual(float(harness.logged[0][1]), 4.0)
400
 
401
  def test_dataset_returns_target_anchor_dynamic_revisit_contract(self):
402
  with tempfile.TemporaryDirectory() as tmp:
 
1
  import tempfile
2
  import unittest
3
+ from unittest import mock
4
  from pathlib import Path
5
 
6
  import numpy as np
 
301
  self.assertEqual(harness.logged[0][0], "training/loss")
302
  self.assertEqual(float(harness.logged[0][1]), 2.0)
303
 
304
+ def test_validation_step_builds_online_memory_from_committed_latents(self):
305
+ import algorithms.dememwm.df_video as df_video
306
  from algorithms.dememwm.df_video import DeMemWMMinecraft, _preprocess_dememwm_latent_batch
307
 
308
+ selection_calls = []
309
+
310
+ def fake_select_memory_indices(poses, target_positions, cfg=None, split="training", rng=None):
311
+ start = int(target_positions[0])
312
+ selection_calls.append((target_positions.tolist(), split, float(cfg.fov_overlap_threshold)))
313
+ return {"anchor": [0], "dynamic": [max(0, start - 1)], "revisit": [max(0, start - 2)]}, {"anchor": [start > 0], "dynamic": [start > 0], "revisit": [start > 2]}
314
+
315
  class FakeDiffusion:
316
  def __init__(self):
317
  self.calls = []
 
327
  "kwargs": kwargs,
328
  }
329
  )
330
+ return torch.full_like(x[: kwargs["frame_memory_segments"]["target"]], float(len(self.calls)))
 
 
 
 
331
 
332
  class Harness:
333
  def __init__(self):
334
+ self.memory_condition_length, self.clip_noise = 3, 0.0
335
+ self.context_frames, self.frame_stack, self.chunk_size = 2, 1, 2
336
+ self.cfg = OmegaConf.create(
337
+ {"memory_selection": {"enabled": True, "max_anchor_frames": 1, "max_dynamic_frames": 1, "max_revisit_frames": 1, "fov_overlap_threshold": 0.75}}
338
+ )
339
  self.diffusion_model = FakeDiffusion()
340
  self.logged = []
341
  self.horizons = []
 
345
 
346
  def _generate_scheduling_matrix(self, horizon):
347
  self.horizons.append(horizon)
348
+ return np.stack([np.full(horizon, level, dtype=np.int64) for level in (2, 1, 0)])
349
 
350
  def _generate_condition_indices(self, *args, **kwargs):
351
+ raise AssertionError("latent dict validation must not use the legacy memory_condition_length path")
352
 
353
  def encode(self, xs):
354
  raise AssertionError("latent dict validation must not VAE-encode inputs")
 
360
  self.logged.append((name, value))
361
 
362
  batch = {
363
+ "latents": torch.tensor([0, 1, 2, 3, 4, 100, 101, 102], dtype=torch.float32).view(1, 8, 1, 1, 1),
364
+ "actions": (100 + torch.arange(8, dtype=torch.float32)).view(1, 8, 1),
365
+ "poses": (200 + torch.arange(8, dtype=torch.float32)).view(1, 8, 1).repeat(1, 1, 5),
366
+ "frame_indices": torch.tensor([[10, 11, 12, 13, 14, 900, 901, 902]], dtype=torch.long),
367
+ "memory_segments": {"target": torch.tensor([5]), "anchor": torch.tensor([1]), "dynamic": torch.tensor([1]), "revisit": torch.tensor([1])},
368
+ "memory_masks": {"target": torch.ones((1, 5), dtype=torch.bool), "anchor": torch.ones((1, 1), dtype=torch.bool), "dynamic": torch.ones((1, 1), dtype=torch.bool), "revisit": torch.ones((1, 1), dtype=torch.bool)},
 
 
 
 
 
 
 
 
 
 
369
  "image_hw": torch.tensor([[360, 640]], dtype=torch.long),
370
  }
371
  harness = Harness()
372
 
373
+ with mock.patch.object(df_video, "select_memory_indices", side_effect=fake_select_memory_indices):
374
+ loss = DeMemWMMinecraft.validation_step(harness, batch, 0, namespace="test")
375
  calls = harness.diffusion_model.calls
376
+ required_kwargs = {"reference_length", "frame_memory_segments", "frame_memory_masks", "frame_memory_pose", "frame_idx", "image_hw"}
377
 
378
+ self.assertEqual((harness.horizons, selection_calls, len(calls)), ([2, 1], [([2, 3], "test", 0.75), ([4], "test", 0.75)], 4))
379
+ self.assertTrue(all(required_kwargs <= call["kwargs"].keys() for call in calls))
380
+ self.assertTrue(all(call["kwargs"]["reference_length"] == 0 for call in calls))
381
+ self.assertEqual(
382
+ ([call["kwargs"]["current_frame"] for call in calls[::2]], [call["kwargs"]["frame_memory_segments"]["target"] for call in calls[::2]]),
383
+ ([2, 4], [2, 1]),
384
+ )
385
+ self.assertEqual(calls[0]["x"][:, 0, 0, 0, 0].tolist(), [0.0, 0.0, 0.0, 1.0, 0.0])
386
+ self.assertEqual(calls[0]["action_cond"][:, 0, 0].tolist(), [102.0, 103.0, 0.0, 0.0, 0.0])
387
+ self.assertEqual(calls[0]["kwargs"]["frame_idx"][:, 0].tolist(), [12, 13, 10, 11, 0])
388
+ self.assertEqual(calls[0]["kwargs"]["frame_memory_masks"]["revisit"].tolist(), [[False]])
389
+ self.assertAlmostEqual(float(loss), 1.0 / 3.0)
 
 
 
 
 
 
 
 
 
 
390
 
391
  def test_dataset_returns_target_anchor_dynamic_revisit_contract(self):
392
  with tempfile.TemporaryDirectory() as tmp: