Files
cicalooo 17f019c061 Blend PDD heads as row 0 plus offsets
Core treats later bank rows as offsets from the first head. set_weight files already use that layout; proj_out banks are converted before the same blend.
2026-10-11 15:31:21 +11:00

229 lines
9.0 KiB
Python

import unittest
from types import SimpleNamespace
import torch
import torch.nn as nn
from h3lora.pdd import PddBank, PddFinalLayer, PddState, _as_bank, _blend, _to_offsets, interval, peel
class PeelTests(unittest.TestCase):
def test_stacked_set_weight(self):
video = torch.randn(3072, 5376)
audio = torch.randn(1024, 5376)
sd = {
"diffusion_model.final_layer.video_out.set_weight": video,
"diffusion_model.final_layer.video_out.set_bias": torch.randn(3072),
"diffusion_model.final_layer.audio_out.set_weight": audio,
"diffusion_model.blocks.0.attn.qkv_proj.lora_A.weight": torch.randn(8, 5376),
}
bank, note = peel(sd, 96, 32, 5376)
self.assertIsNotNone(bank)
self.assertEqual(bank["n"], 32)
self.assertEqual(bank["video_w"].shape, (3072, 5376))
self.assertEqual(bank["audio_w"].shape, (1024, 5376))
# set_weight is already row 0 plus offsets; do not subtract again.
self.assertTrue(torch.equal(bank["video_w"], video))
self.assertTrue(torch.equal(bank["audio_w"], audio))
self.assertNotIn("offsets", note)
self.assertIn("n=32", note)
self.assertNotIn("set_weight", sd)
self.assertIn("diffusion_model.blocks.0.attn.qkv_proj.lora_A.weight", sd)
def test_proj_out_3d(self):
video = torch.randn(32, 96, 5376)
video_b = torch.randn(32, 96)
audio = torch.randn(32, 32, 5376)
audio_b = torch.randn(32, 32)
sd = {
"proj_out.weight": video.clone(),
"proj_out.bias": video_b.clone(),
"audio_proj_out.weight": audio.clone(),
"audio_proj_out.bias": audio_b.clone(),
}
bank, note = peel(sd, 96, 32, 5376)
self.assertEqual(bank["n"], 32)
self.assertEqual(bank["video_w"].shape, (3072, 5376))
self.assertEqual(bank["video_b"].shape, (3072,))
self.assertEqual(bank["audio_w"].shape, (1024, 5376))
self.assertFalse(sd)
self.assertIn("video full heads stored as offsets", note)
self.assertIn("audio full heads stored as offsets", note)
self.assertTrue(torch.allclose(
bank["video_w"], _to_offsets(video.reshape(3072, 5376), 96)))
self.assertTrue(torch.allclose(
bank["video_b"], _to_offsets(video_b.reshape(3072), 96)))
self.assertTrue(torch.allclose(
bank["audio_w"], _to_offsets(audio.reshape(1024, 5376), 32)))
def test_native_head_left_alone(self):
w = torch.randn(96, 5376)
sd = {"diffusion_model.final_layer.video_out.set_weight": w}
bank, note = peel(sd, 96, 32, 5376)
self.assertIsNone(bank)
self.assertEqual(note, "")
self.assertIs(sd["diffusion_model.final_layer.video_out.set_weight"], w)
def test_garbage_dropped(self):
sd = {"diffusion_model.final_layer.video_out.set_weight": torch.randn(100, 5376)}
bank, note = peel(sd, 96, 32, 5376)
self.assertIsNone(bank)
self.assertIn("dropped", note)
self.assertFalse(sd)
class BlendTests(unittest.TestCase):
def test_as_bank_3d_and_2d(self):
t = torch.arange(2 * 4 * 3, dtype=torch.float32).reshape(2, 4, 3)
n, flat = _as_bank(t, 4)
self.assertEqual(n, 2)
self.assertEqual(flat.shape, (8, 3))
n2, flat2 = _as_bank(flat, 4)
self.assertEqual(n2, 2)
self.assertTrue(torch.equal(flat, flat2))
def test_single_later_interval_is_base_plus_offset(self):
n, o, hidden = 4, 2, 3
weight = torch.zeros(n * o, hidden)
bias = torch.zeros(n * o)
for i in range(n):
weight[i * o:(i + 1) * o] = float(i + 1)
bias[i * o:(i + 1) * o] = float(i + 1)
h = torch.ones(5, hidden)
out = _blend(weight, bias, h, n, 2, 3, 1.0)
# row 0 is 1, row 2 is an offset of 3, so the head is 4.
self.assertEqual(out.shape, (5, o))
self.assertTrue(torch.allclose(out, torch.full((5, o), 4.0 * hidden + 4.0)))
def test_opening_interval_is_row_zero(self):
n, o, hidden = 4, 2, 3
weight = torch.zeros(n * o, hidden)
for i in range(n):
weight[i * o:(i + 1) * o] = float(i + 1)
h = torch.ones(2, hidden)
out = _blend(weight, None, h, n, 0, 1, 12.0)
self.assertTrue(torch.allclose(out, torch.full((2, o), 1.0 * hidden)))
def test_span_matches_core_offset_formula(self):
n, o, hidden = 4, 2, 5
weight = torch.randn(n * o, hidden)
bias = torch.randn(n * o)
h = torch.randn(3, hidden)
start, stop, shift = 0, 3, 12.0
out = _blend(weight, bias, h, n, start, stop, shift)
grid = torch.linspace(1.0, 0.0, n + 1, dtype=torch.float64)
dt = (1.0 - shift * grid / (1.0 + (shift - 1.0) * grid)).diff()[start:stop]
w = (dt / dt.sum()).to(h.dtype)
rows = weight.reshape(n, o, hidden)
brows = bias.reshape(n, o)
first = max(start, 1)
expect_w = rows[0] + torch.einsum("n,noi->oi", w[first - start:], rows[first:stop])
expect_b = brows[0] + torch.einsum("n,no->o", w[first - start:], brows[first:stop])
self.assertTrue(torch.allclose(out, torch.nn.functional.linear(h, expect_w, expect_b)))
def test_converted_full_head_reconstructs_that_interval(self):
n, o, hidden = 4, 2, 3
heads = torch.stack([torch.full((o, hidden), float(i + 1)) for i in range(n)])
offsets = _to_offsets(heads.reshape(n * o, hidden), o)
h = torch.ones(2, hidden)
out = _blend(offsets, None, h, n, 2, 3, 12.0)
self.assertTrue(torch.allclose(out, torch.full((2, o), 3.0 * hidden)))
def test_interval_monotonic(self):
start, stop = interval(torch.tensor(1.0), torch.tensor(0.0), 32, 12.0)
self.assertGreaterEqual(stop, start + 1)
self.assertGreaterEqual(start, 0)
self.assertLessEqual(stop, 32)
def test_partial_audio_bank_falls_back_to_native_video(self):
sd = {
"final_layer.audio_out.set_weight": torch.randn(2 * 2, 3),
}
bank, note = peel(sd, 4, 2, 3)
self.assertIsNotNone(bank)
self.assertIsNone(bank["video_w"])
self.assertIn("n=2", note)
def test_strength_interpolates_native_and_pdd_heads(self):
class StaticAdaln(nn.Module):
def forward(self, _t):
return torch.zeros(2, 3), torch.zeros(2, 3)
inner = SimpleNamespace(
norm=nn.Identity(),
adaln_proj=StaticAdaln(),
video_out=nn.Linear(3, 2),
audio_out=nn.Linear(3, 1),
)
bank, _ = peel({
"final_layer.video_out.set_weight": torch.ones(2 * 2, 3),
}, 2, 1, 3)
state = PddState(0.5, None)
state.set(torch.tensor(1.0), torch.tensor([1.0, 0.0]), (12.0, 3.0), 0.5)
layer = PddFinalLayer(inner, PddBank(bank), state)
video, audio = layer(
torch.ones(4, 3), torch.zeros(1, 3), (0, 2, 0), (2, 4, 1))
native = inner.video_out(torch.ones(2, 3))
start, stop = interval(torch.tensor(1.0), torch.tensor(0.0), 2, 12.0)
pdd = _blend(layer.bank.video_w, layer.bank.video_b, torch.ones(2, 3), 2, start, stop, 12.0)
self.assertTrue(torch.allclose(video, native + 0.5 * (pdd - native)))
self.assertEqual(audio.shape, (2, 1))
def test_current_comfy_signature_prefers_direct_schedule_arguments(self):
class StaticAdaln(nn.Module):
def forward(self, _t):
return torch.zeros(2, 3), torch.zeros(2, 3)
inner = SimpleNamespace(
norm=nn.Identity(),
adaln_proj=StaticAdaln(),
video_out=nn.Linear(3, 2),
audio_out=nn.Linear(3, 1),
)
video_bank = torch.cat([
torch.full((2, 3), float(head))
for head in (1, 2, 3, 4)
])
bank, _ = peel({
"final_layer.video_out.set_weight": video_bank,
}, 2, 1, 3)
state = PddState(1.0, None)
# Deliberately unusable legacy schedule data: direct arguments from
# current ComfyUI must take precedence, while strength still comes from
# the outer stack wrapper's state.
state.set(torch.tensor(0.5), torch.empty(0), (1.0, 1.0), 1.0)
layer = PddFinalLayer(inner, PddBank(bank), state)
sigma = torch.tensor(1.0)
sample_sigmas = torch.tensor([1.0, 0.0])
shifts = (12.0, 3.0)
x = torch.ones(4, 3)
video, audio = layer(
x,
torch.zeros(1, 3),
(0, 2, 0),
(2, 4, 1),
sigma,
sample_sigmas,
shifts,
)
start, stop = interval(sigma, sample_sigmas[1], 4, shifts[0])
expected_video = _blend(
layer.bank.video_w,
layer.bank.video_b,
x[:2],
4,
start,
stop,
shifts[0],
)
self.assertTrue(torch.allclose(video, expected_video))
self.assertEqual(audio.shape, (2, 1))
if __name__ == "__main__":
unittest.main()