Repository navigation
Expand file tree
/
Copy pathnegative_memory.py
More file actions
147 lines (128 loc) · 6.22 KB
/
Copy pathnegative_memory.py
File metadata and controls
147 lines (128 loc) · 6.22 KB
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
"""DynaShift negative latent memory.
Per-refinement-key store of generations the user rated as containing something
UNWANTED (an extra person, wrong appearance, ...). The Scene Chain sampler saves the
final video latent of every keyed run as a PENDING entry; the rating that scores that
run either promotes it into the bank (negative ratings) or discards it. At sampling
time the bank acts as a latent-space negative prompt at CFG=1: see
samplers._build_dynashift_wrapper for the steering math.
This is the latent-space sibling of the Refiner's conditioning-space `bad_dir`:
bad_dir repels the INPUT (what we ask for), the negative bank repels the OUTPUT
(what actually appeared). Files ride the refinement-key sidecar convention
(<key>.negative_latents.pt / <key>.pending_negative.pt), so delete_refinement_key's
stem glob removes them atomically with the key - no orphaned-sidecar drift.
"""
import os
import time
import torch
# Bank size: enough to cover the recurring failure modes of one key without turning
# the store into a museum. Oldest entries roll off; keys are disposable anyway.
MAX_NEGATIVES = 8
# V2 rating profile keys whose semantics are "something appeared that I did not want" -
# these bank regardless of reward (wrong_appearance carries reward 0.0 because it is a
# repair signal, but it is THE canonical intrusion rating).
NEGATIVE_RATING_KEYS = ("awful", "wrong_appearance")
# Ratings without an intrusion label still bank when the profile reward marks the run a
# genuinely bad OUTCOME: the quality-missing family (Missing quality -0.25 through
# Missing details+action+quality -0.65) - degradation is visible in the latent, so
# steering off that attractor is meaningful. Deliberately excludes the near-miss
# ratings with positive reward (Missing details +0.35, Missing action +0.05): those
# latents are mostly-correct content, and banking them would repel future runs from
# material the user actually wants.
NEGATIVE_REWARD_THRESHOLD = -0.25
def is_negative_profile(learning_profile):
"""True when a V2 rating profile should feed the negative bank."""
if not isinstance(learning_profile, dict):
return False
if learning_profile.get("key") in NEGATIVE_RATING_KEYS:
return True
try:
return float(learning_profile.get("reward", 0.0)) <= NEGATIVE_REWARD_THRESHOLD
except (TypeError, ValueError):
return False
def _state_path(refinement_key, mode):
try:
from .conditioning import refinement_state_path
except ImportError:
from conditioning import refinement_state_path
return refinement_state_path(refinement_key, mode, prefix="refine_v2", extension="pt")
def _atomic_save(payload, path):
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = path + ".tmp"
torch.save(payload, tmp)
os.replace(tmp, path) # a concurrent read never sees a partial write
def save_pending(refinement_key, video_latent, conditioning=None):
"""Persist this run's final video latent (+ mean-pooled conditioning) as the
pending candidate the NEXT rating will judge. fp16 CPU, batch squeezed; one
pending per key (a new run overwrites - each rating scores its own run)."""
if not refinement_key or not isinstance(video_latent, torch.Tensor):
return False
try:
lat = video_latent.detach()
if lat.dim() >= 4 and lat.shape[0] == 1:
lat = lat.squeeze(0)
cond_vec = None
if isinstance(conditioning, torch.Tensor):
c = conditioning.detach().float()
if c.dim() == 3:
c = c.squeeze(0)
if c.dim() == 2:
c = c.mean(dim=0)
cond_vec = c.to(torch.float16).cpu()
_atomic_save({
"latent": lat.to(torch.float16).cpu(),
"cond": cond_vec,
"stamp": time.strftime("%Y-%m-%d_%H-%M-%S"),
}, _state_path(refinement_key, "pending_negative"))
return True
except Exception as e:
print(f"[FunPack DynaShift] pending negative save failed: {e}")
return False
def consume_pending(refinement_key, promote, rating_key=None):
"""Pair the pending latent with the rating that scores its run: promote it into
the negative bank (ring buffer) or discard it. Always removes the pending file so
a stale candidate can never be promoted by a later, unrelated rating. Returns the
bank size after promotion, or None when nothing was promoted. `rating_key` is
stored on the entry as provenance (which rating banked it - available for
severity-weighted steering later)."""
if not refinement_key:
return None
pending_path = _state_path(refinement_key, "pending_negative")
if not os.path.exists(pending_path):
return None
count = None
try:
if promote:
pending = torch.load(pending_path, map_location="cpu", weights_only=False)
if isinstance(pending, dict) and isinstance(pending.get("latent"), torch.Tensor):
if rating_key:
pending["rating"] = str(rating_key)
store_path = _state_path(refinement_key, "negative_latents")
entries = load_negatives(refinement_key)
entries.append(pending)
entries = entries[-MAX_NEGATIVES:]
_atomic_save({"version": 1, "entries": entries}, store_path)
count = len(entries)
except Exception as e:
print(f"[FunPack DynaShift] negative bank update failed: {e}")
finally:
try:
os.remove(pending_path)
except OSError:
pass
return count
def load_negatives(refinement_key):
"""Bank entries ([{latent: fp16 [C,T,H,W], cond: fp16 [D]|None, stamp}], oldest
first), or [] when the key has no bank yet."""
if not refinement_key:
return []
path = _state_path(refinement_key, "negative_latents")
if not os.path.exists(path):
return []
try:
data = torch.load(path, map_location="cpu", weights_only=False)
entries = data.get("entries") if isinstance(data, dict) else None
return [e for e in (entries or [])
if isinstance(e, dict) and isinstance(e.get("latent"), torch.Tensor)]
except Exception as e:
print(f"[FunPack DynaShift] negative bank load failed: {e}")
return []