File size: 18,541 Bytes
00c7b31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# Adding an Attention-Based Memory Module to Echo-Memory

This guide explains how to add a **new attention-based memory mechanism** to the Wan 2.1 backbone in this repository, following the exact integration pattern used by the **Block-wise SSM** row (`diffsynth/models/memory/block_wise_ssm.py`). Block-wise SSM is the right template because it is a **per-DiT-block module**: it is instantiated inside each selected transformer block and called during the block's forward pass β€” precisely where an attention variant would live.

Read `doc/memory_mechanisms.md` and the repo `CLAUDE.md` first for the two-chunk paradigm and the public-repo constraints. This document assumes that background.

---
I want to add some loss, for example, using the model's uncertainty about the current prediction, um: e^(-um)*sg(MSE(VAE(Target View), predicted x0)) + um, as a metric for the model's retrieval, thereby improving memory capabilities. Please return the code implementation of this method in the current codebase. In addition, please provide methods you believe can improve the model's ability to retrieve preceding frames, and compile them into a retrieve.md file and save it.
## 0. How per-block memory is wired (the pattern you will copy)

Unlike the FramePack/Spatial rows (which act in the pipeline on context latents), Block-wise SSM attaches **inside the DiT transformer blocks**. The same block class is defined in **two places that must stay in sync**:

| Location | Role |
| --- | --- |
| `src/model_training/train.py` β†’ `class DiTBlock_w_Action` (~line 160) | **Training** definition of the augmented block |
| `env/loop_utils.py` β†’ `class DiTBlock_w_Action` (~line 43) | **Inference** definition (must match training byte-for-byte in module structure) |

The base Wan blocks (`diffsynth/models/wan_video_dit.py`) are **replaced** at load time by `DiTBlock_w_Action`, copying over `self_attn / cross_attn / norm* / ffn / modulation` weights from the original blocks. The memory module is an *extra* sub-module on selected blocks; its weights are the only newly-trained parameters.

**The forward hook location** (train.py `DiTBlock_w_Action.forward`, ~line 230-238):

```python
input_x = modulate(self.norm1(x), shift_msa, scale_msa)
x = self.gate(x, gate_msa, self.self_attn(input_x, freqs))   # ← self-attention
if num_frames is not None:
    if hasattr(self, "block_wise_ssm"):
        x = self.block_wise_ssm(x, f=num_frames)             # ← MEMORY HOOK (after self-attn, before cross-attn)
x = x + self.cross_attn(self.norm3(x), context)              # ← cross-attention
input_x = modulate(self.norm2(x), shift_mlp, scale_mlp)
x = self.gate(x, gate_mlp, self.ffn(input_x))
```

`x` here is shape `(B, F*S, D)` β€” batch, (latent frames Γ— spatial tokens) flattened, hidden dim. `num_frames` (`f`) is the number of latent frames, used to reshape per-frame. **This is where your attention-memory module reads/writes.**

**Detection at inference is by checkpoint key name.** `env/loop_utils.py` (~line 288) scans ckpt keys with a regex and decides which blocks get the module:

```python
m = re.match(r"blocks\.(\d+)\.block_wise_ssm\.", key)   # β†’ block_wise_block_ids
```

So the *attribute name* you give your module (e.g. `self.attn_memory = ...`) becomes the checkpoint key prefix and must be matched by a new regex. **Keep the attribute name stable** β€” `env/memory_baseline_runtime.py` and `inference/unified_inference.py` rely on it.

---

## 1. Files to ADD

### 1a. The module β€” `diffsynth/models/memory/attn_memory.py`

Mirror the shape contract of `block_wise_ssm.py`: input `(B, F*S, D)`, takes `f` (frames), returns the same shape with a **zero-initialized residual gate** so an untrained module is an identity at step 0 (critical β€” the base backbone must not be disturbed before training).

```python
import torch
import torch.nn as nn
import torch.nn.functional as F


class AttentionMemory(nn.Module):
    """
    Example attention-based memory module attached per DiT block.

    Reads the within-block hidden state and applies an extra attention
    operation along the TIME axis for each spatial-token trajectory
    (analogous to block_wise_ssm's recurrent time update, but attention).

    Shape contract (must match block_wise_ssm):
      forward(x, f) where x: (B, F*S, D), f = number of latent frames.
    """

    def __init__(self, dim: int, num_heads: int = 8):
        super().__init__()
        self.dim = int(dim)
        self.num_heads = int(num_heads)
        self.q = nn.Linear(dim, dim)
        self.k = nn.Linear(dim, dim)
        self.v = nn.Linear(dim, dim)
        self.o = nn.Linear(dim, dim)
        # Zero-init residual gate => identity at init (do NOT skip this).
        self.gate = nn.Parameter(torch.zeros(1))

    def forward(self, x: torch.Tensor, f: int, **_kwargs):
        if x is None or x.ndim != 3:
            return x
        b, n, d = x.shape
        f = int(f or 0)
        if d != self.dim or f <= 1 or n % f != 0:
            return x

        spatial = n // f
        # (B, F*S, D) -> (B*S, F, D): attend across frames per spatial trajectory.
        x_seq = x.reshape(b, f, spatial, d).permute(0, 2, 1, 3).reshape(b * spatial, f, d)

        h = self.num_heads
        q = self.q(x_seq).reshape(b * spatial, f, h, d // h).transpose(1, 2)
        k = self.k(x_seq).reshape(b * spatial, f, h, d // h).transpose(1, 2)
        v = self.v(x_seq).reshape(b * spatial, f, h, d // h).transpose(1, 2)
        y = F.scaled_dot_product_attention(q, k, v)                 # causal=False -> full temporal memory
        y = y.transpose(1, 2).reshape(b * spatial, f, d)
        y = self.o(y)

        y = y.reshape(b, spatial, f, d).permute(0, 2, 1, 3).reshape(b, n, d)
        return x + torch.tanh(self.gate) * y                        # gated residual
```

> Vary the internal operation to run the experiment you care about β€” causal vs. bidirectional temporal attention, cross-attention into stored context tokens, windowed/strided attention, etc. The **interface and the gated-residual identity-at-init must stay fixed**; only the body changes.

### 1b. Export it β€” `diffsynth/models/memory/__init__.py`

Add alongside the existing exports:

```python
from .attn_memory import AttentionMemory
```

### 1c. The training launcher β€” `train/memory_baselines_basic/run_ablation_attn_memory_two_chunk.sh`

Copy `run_ablation_block_wise_ssm_two_chunk.sh` and swap the memory flags (see Β§4).

---

## 2. Files to MODIFY for TRAINING (`src/model_training/train.py`)

There are **five** edit sites. Search for `block_wise_ssm` to find each by analogy.

**(1) Module import** (~line 77, plus the `modules_to_clear` list ~line 44 so hot-reload works):

```python
from diffsynth.models.memory.attn_memory import AttentionMemory
# and add 'diffsynth.models.memory.attn_memory' to modules_to_clear
```

**(2) `DiTBlock_w_Action.__init__`** (~line 160) β€” add a constructor flag and instantiate:

```python
def __init__(self, ..., use_block_wise_ssm=False, use_videossm_hybrid=False,
             use_attn_memory: bool = False, attn_memory_heads: int = 8, ...):
    ...
    self.use_attn_memory = bool(use_attn_memory)
    if use_attn_memory:
        self.attn_memory = AttentionMemory(dim, num_heads=attn_memory_heads)
```

**(3) `DiTBlock_w_Action.forward`** (~line 232) β€” add the hook next to the SSM hook:

```python
if num_frames is not None:
    if hasattr(self, "block_wise_ssm"):
        x = self.block_wise_ssm(x, f=num_frames)
    if hasattr(self, "attn_memory"):
        x = self.attn_memory(x, f=num_frames)        # ← new
```

**(4) argparse flags** (~line 1479-1510). Add the numeric default tuples near `--ssm_every_n_blocks`, and the boolean flag to the store-true list (~line 1510 alongside `--use_block_wise_ssm`):

```python
("--attn_memory_every_n_blocks", dict(type=int, default=4)),
("--attn_memory_heads",          dict(type=int, default=8)),
# ... and add "--use_attn_memory" to the list of store_true flags
```

Optionally add a mutual-exclusion guard like the one at ~line 1556 (`use_block_wise_ssm and use_videossm_hybrid`) if your module should not co-exist with another per-block memory.

**(5) block instantiation + parameter freezing** (~line 1695-1761):

```python
# instantiation loop (~line 1695)
use_attn_memory = bool(_arg('use_attn_memory', False))
attn_every_n   = max(int(_arg('attn_memory_every_n_blocks', 4) or 4), 1)
...
for block_id, old_block in enumerate(old_blocks):
    attach_block_ssm  = use_block_wise_ssm and (block_id % ssm_every_n == 0)
    attach_attn_mem   = use_attn_memory   and (block_id % attn_every_n == 0)   # ← new
    new_block = DiTBlock_w_Action(
        ...,
        use_block_wise_ssm=attach_block_ssm,
        use_attn_memory=attach_attn_mem,                                       # ← new
        attn_memory_heads=int(_arg('attn_memory_heads', 8) or 8),
    )
```

Then add `"attn_memory"` to **all three** `requires_grad` filters (~line 1744, 1751, 1758) so only your module (plus action MLP / self-attn-with-action) trains and the rest of the DiT stays frozen:

```python
if "action_mlp" in name or "self_attn_with_action" in name \
   or "block_wise_ssm" in name or "videossm_hybrid" in name \
   or "attn_memory" in name:          # ← new
    param.requires_grad = True
else:
    param.requires_grad = False
```

> ⚠️ If you forget the freeze filter, your module will not be trained (it'll be frozen with everything else), or the whole DiT becomes trainable β€” both break the controlled-ablation premise.

---

## 3. Files to MODIFY for INFERENCE / EVAL

### 3a. `env/loop_utils.py` β€” mirror the block and detect from ckpt keys

This file re-defines `DiTBlock_w_Action` and rebuilds blocks at load time. **It must structurally match `train.py`.** Three edits:

1. **Import + module attribute** in its `DiTBlock_w_Action` (~line 43-75): add the `use_attn_memory` ctor arg, `self.attn_memory = AttentionMemory(dim, ...)`, and the same forward hook (~line 111).
2. **`_build_action_blocks`** (~line 202): add an `attn_memory_block_ids` set param and pass `use_attn_memory=block_id in attn_memory_block_ids` into each block.
3. **`load_pipeline_and_ckpt`** (~line 288): add a detection regex so the right blocks are reconstructed before loading weights:

```python
m = re.match(r"blocks\.(\d+)\.attn_memory\.", key)
if m:
    attn_memory_block_ids.add(int(m.group(1)))
```

and thread `attn_memory_block_ids` into the `_build_action_blocks(...)` call (~line 304).

> Because detection is **automatic from checkpoint keys**, no inference flag is needed to turn the module on β€” the presence of `blocks.N.attn_memory.*` keys in the safetensors reconstructs the slots. This mirrors how `block_wise_ssm` / `videossm_hybrid` work today.

### 3b. `env/memory_baseline_runtime.py` β€” register the profile

Three edits (search `block_wise_ssm`):

1. **`MemoryProfile` dataclass** (~line 46): add `use_attn_memory: bool = False`.
2. **`MEMORY_PROFILE_REGISTRY`** (~line 308): add a spec. The `ckpt_substrings` must match your launcher's `--output_path` folder name:

```python
MemoryProfileSpec(
    profile_id="attn_memory_two_chunk",
    ckpt_substrings=("memory_baselines_basic_abl_attn_memory_two_chunk",),
    paper_tag="attention_memory",
    train_flags=("--use_attn_memory", "--context_memory_frames 5"),
    infer_flags=("load_pipeline_and_ckpt auto-infers Attn-memory from attn_memory.* ckpt keys",),
    eval_flags=("--context_frames 5",),
    profile=MemoryProfile(use_attn_memory=True, context_override=5),
),
```

3. **`profile_to_argv`** (~line 393) and **`apply_memory_baseline_pipe`** (~line 427): add the `--use_attn_memory` emission and `pipe.use_attn_memory = ...` line, mirroring the block_wise_ssm entries.

### 3c. `inference/unified_inference.py` β€” add the alias

In `_REGISTRY_ALIAS` (~line 72) add:

```python
"attn_memory": "attn_memory_two_chunk",
```

and (~line 138) `pipe.use_attn_memory = bool(getattr(profile, "use_attn_memory", False))`. Update the `--help` memory-type list near line 172.

---

## 4. The training launcher

`train/memory_baselines_basic/run_ablation_attn_memory_two_chunk.sh` β€” copy the block-wise SSM launcher and change only the memory flags, run name, and output folder. The critical line:

```bash
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/common_env.sh"
source "${SCRIPT_DIR}/common_sampling_two_chunk.sh"
accelerate launch src/model_training/train.py \
  --dataset_base_path "${dataset_base_path}" --dataset_metadata_path "${dataset_base_path}/${METADATA_NAME}" \
  --context_source replay --prev_chunk_frames 81 --context_memory_frames 5 --use_rt_relative --height 352 --width 640 \
  --dataset_repeat 1 --per_device_train_batch_size 1 --gradient_accumulation_steps 1 --num_workers 16 \
  --model_paths "${model_paths}" --learning_rate 5e-5 --num_epochs 1 --remove_prefix_in_ckpt "${remove_prefix_in_ckpt}" \
  --output_path "${output_base}_abl_attn_memory_two_chunk" --trainable_models dit --ckpt_interval "${CKPT_INTERVAL:-1000}" --save_full_model \
  --wandb_run_name "abl_attn_memory_two_chunk" \
  --enable_context_memory --training_mode context --context_drop_prob 0.1 --cfg_target_only \
  --train_cam_pose --add_action_attn --action_use_temporal_attention \
  --use_moc --moc_temperature 1.0 \
  --use_attn_memory --attn_memory_every_n_blocks 4 --attn_memory_heads 8 \
  --timestep_shift "${TIMESTEP_SHIFT:-15}" \
  "${SAMPLING_TWO_CHUNK_FLAGS[@]}" \
  2>&1 | tee "${LOG_DIR}/abl_attn_memory_two_chunk_$(date +%Y%m%d_%H%M%S).log"
```

> The `--output_path` folder name (`..._abl_attn_memory_two_chunk`) **must contain the `ckpt_substrings` token** you registered in Β§3b, so eval/inference auto-resolve the profile from the checkpoint path. Keep all the shared hyperparameters (LR 5e-5, 352Γ—640, 81 frames, shift 15) identical to the other rows β€” that is what makes this a *controlled* ablation; only the memory pathway should differ.

---

## 5. Running the experiment

**Environment** (repo root):

```bash
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
export PYTHONPATH=$PWD:${PYTHONPATH:-}
export OUTPUT_BASE_ROOT=$PWD/outputs
```

**Sanity-check the plumbing first** β€” the existing standalone tests cover context/two-chunk wiring; run them after your edits:

```bash
PYTHONPATH=. python3 tests/test_context_chunk_utils.py
PYTHONPATH=. python3 tests/test_two_chunk_anchor_readout.py
```

Add a tiny shape test for your module (recommended), asserting identity at init and `(B,F*S,D)` round-trip:

```python
import torch
from diffsynth.models.memory.attn_memory import AttentionMemory
m = AttentionMemory(64, num_heads=8)
x = torch.randn(2, 5*16, 64)            # B=2, F=5, S=16, D=64
assert torch.allclose(m(x, f=5), x)     # zero gate => identity at init
```

**Train** (optionally do a quick run with a small metadata index first):

```bash
# Optional: 1000-row index for a fast smoke run
OUTPUT_CSV="${DATASET_BASE_PATH}/metadata_1000.csv" METADATA_MAX_ROWS=1000 bash scripts/run_generate_metadata.sh

bash train/memory_baselines_basic/run_ablation_attn_memory_two_chunk.sh
```

Checkpoints land under `outputs/memory_baselines_basic_abl_attn_memory_two_chunk/epoch-*.safetensors`. **Verify the new weights are present and gradients flowed:**

```bash
python -c "
from safetensors.torch import load_file
k = load_file('outputs/memory_baselines_basic_abl_attn_memory_two_chunk/epoch-0.safetensors')
hits = [x for x in k if 'attn_memory' in x]
print(len(hits), 'attn_memory tensors; sample:', hits[:3])
"
```

**Inference** (auto-detected from ckpt keys; `--memory_type auto` works once Β§3 is done):

```bash
python inference/unified_inference.py \
  --ckpt outputs/memory_baselines_basic_abl_attn_memory_two_chunk/epoch-0.safetensors \
  --memory_type attn_memory \
  --context_image assets/opendomain_revisit/1774363417.png \
  --action_path env/action_rotation_left_45.json \
  --prompt "A toy bear on a table" \
  --output_path attn_memory_test.mp4
```

**Evaluate** (the row folder name in `CKPT` drives profile selection):

```bash
export CKPT=outputs/memory_baselines_basic_abl_attn_memory_two_chunk/epoch-0.safetensors
bash eval/v2/run_basic_replay_gt.sh                          # fast fidelity check (~5 min)
bash eval/v2/run_static_consistency_loop_and_revisit.sh      # full paper bundle (loop closure + revisit)
PHASE=stage1 OOD_DIR=assets/opendomain_revisit \
  bash eval/v2/revisit_suite/run_one_click_revisit_eval.sh   # open-domain generalization
```

Compare the resulting MSE / PSNR / SSIM / LPIPS (and revisit-tail consistency) against the `block_wise_ssm`, `spatial_mem`, and `context_k*` rows to place your attention-memory variant in the paper matrix.

---

## 6. Checklist & pitfalls

- [ ] `diffsynth/models/memory/attn_memory.py` added; exported in `__init__.py`.
- [ ] `train.py`: import + `modules_to_clear` + ctor + forward hook + argparse + instantiation + **all three freeze filters**.
- [ ] `env/loop_utils.py`: `DiTBlock_w_Action` mirrors training **exactly** (structure, attribute name, hook order), + detection regex + `_build_action_blocks` thread-through.
- [ ] `env/memory_baseline_runtime.py`: `MemoryProfile` field + registry spec + `profile_to_argv` + `apply_memory_baseline_pipe`.
- [ ] `inference/unified_inference.py`: alias + pipe flag + help text.
- [ ] Launcher `--output_path` folder name contains the registry `ckpt_substrings` token.

**Common failure modes**

- **Module attribute name mismatch** between train/inference, or vs. the detection regex β†’ checkpoint keys won't reconstruct the slots; weights load as "unexpected" and silently do nothing. Keep `attn_memory` consistent everywhere.
- **Non-zero gate at init** β†’ corrupts the frozen backbone before training and destabilizes early steps. Always zero-init the residual gate.
- **Forgotten freeze filter** β†’ either the module isn't trained, or the whole DiT trains (breaks the controlled comparison and the `--save_full_model` checkpoints diff against base unexpectedly).
- **`loop_utils.DiTBlock_w_Action` drifting from `train.py`** β†’ load-time block replacement mismatches and `strict=False` hides it as missing/unexpected keys. When you edit one, edit both.

**Public-repo constraints** (from `CLAUDE.md`): no machine-local absolute paths, no upload scripts, keep diffs minimal and match existing bash/Python patterns. Don't commit `outputs/`, `data/`, or weights.