feat(lora): pdd exports with a trained sigma grid and replaced weights

Inference-only PDD exports such as alibaba-pai's Qwen-Image-2.1-Fun-Acc-4Step carry no pdd_num_steps: the grid is pdd_sigmas in the metadata or in the pdd_config.json the release keeps beside the weights, and the file replaces norm weights outright. Those parameters are set with the heads, blended toward the loaded value by strength and restored on removal. The pin passes the grid's block boundaries as sigmas and turns off dynamic shifting, static shift and terminal stretch on that generation's scheduler, which would otherwise reshape them, and leaves num_timesteps alone where the pipeline exposes it as a read-only property. Qwen-Image 2.1 gets an ArchSpec and the PDD loader in its chain; a file with heads but no grid warns and loads its backbone only.
This commit is contained in:
CalamitousFelicitousness
2026-09-30 08:09:08 +01:00
parent c5c09d6cc9
commit d1d3c7c438
4 changed files with 332 additions and 42 deletions
+1
View File
@@ -56,6 +56,7 @@ Plus quite a few other improvements and inevitable bug-fixes, see full changelog
- edit-models: simplify usage of multi-image reference editing
automatically handle: flux.2, glm-image, nano-banana, joy-image-edit, qwen-image-edit
- lora: native loading for qwen-image 2.1, including the fused mlp layout ai-toolkit and musubi-tuner train against
- lora: alibaba-pai qwen-image 2.1 fun-acc 4-step runs on the sigma grid it was distilled for
- processors: `DWPose`, `RTMW`, `RTMO`: image processor updates, thanks @kirtasshh
refactored to use `ONNX` instead of obsolete `mmpose` lib and with additional configurable settings
- **UI**
+170 -41
View File
@@ -4,10 +4,15 @@ A PDD file pairs a backbone LoRA with the output projections repeated once per i
training grid; each step fuses the heads of its block into one projection, so N / block_size evaluations
walk the trajectory. Heads ride on ``Network.extras['pdd']``: ``reconcile`` installs and removes them with
the loaded set, ``pin`` holds the step count and schedule the file was distilled for.
Inference-only exports can also carry the grid itself as ``pdd_sigmas`` (or in the ``pdd_config.json`` the
release keeps beside its weights) and parameters that replace the loaded values outright, such as fine-tuned
norm weights; the pin then hands the scheduler that exact grid.
"""
import os
import copy
import json
import weakref
import torch
from modules.logger import log
@@ -18,6 +23,9 @@ debug_log = log.trace if os.environ.get('SD_LORA_DEBUG', None) is not None else
METADATA_STEPS = 'pdd_num_steps'
METADATA_BLOCK = 'pdd_block_size'
METADATA_SIGMAS = 'pdd_sigmas'
GRID_KEYS = (METADATA_STEPS, METADATA_BLOCK, METADATA_SIGMAS)
SIDECAR = 'pdd_config.json'
EXTRAS_KEY = 'pdd'
@@ -35,46 +43,73 @@ class ArchSpec:
class ParallelHeads:
"""The head tensors of one file and the grid they were trained on."""
"""The head tensors of one file, the grid they were trained on and the parameters the file replaces outright."""
def __init__(self, num_steps, block_size, heads):
def __init__(self, num_steps, block_size, heads, sigmas=None, replaced=None):
self.num_steps = num_steps
self.block_size = block_size
self.heads = heads # head path -> (weight [N, out, in], bias [N, out] or None)
self.sigmas = sigmas # the training grid descending from 1 to 0, or None to take it from the scheduler
self.replaced = replaced or {} # parameter path -> the value the file sets
self.nfe = num_steps // block_size
class Installed:
"""Bookkeeping for the heads currently swapped into a pipeline."""
def __init__(self, name, strength, component, modules, heads, spec):
def __init__(self, name, strength, component, modules, heads, spec, replaced=None):
self.name = name
self.strength = strength
self.component = component
self.modules = modules # head path -> (parent, attribute, original module)
self.heads = heads
self.spec = spec
self.replaced = replaced or {} # parameter path -> (parameter, original data)
self.steps = spec.steps_for(heads.nfe)
def grid_sigmas(metadata):
"""The explicit training grid under ``pdd_sigmas`` as float64, None when absent, ValueError unless it descends from 1 to 0."""
raw = (metadata or {}).get(METADATA_SIGMAS)
if raw is None:
return None
values = json.loads(raw) if isinstance(raw, str) else list(raw)
sigmas = torch.tensor([float(value) for value in values], dtype=torch.float64)
if sigmas.numel() < 2 or sigmas[0] != 1 or sigmas[-1] != 0 or not bool((sigmas.diff() < 0).all()):
raise ValueError(f'sigmas={values}')
return sigmas
def detect(metadata):
"""The (num_steps, block_size) grid a file declares; None without PDD metadata, ValueError for an unusable grid."""
metadata = metadata or {}
if METADATA_STEPS not in metadata:
sigmas = grid_sigmas(metadata)
if METADATA_STEPS not in metadata and sigmas is None:
return None
num_steps = int(metadata[METADATA_STEPS])
num_steps = int(metadata[METADATA_STEPS]) if METADATA_STEPS in metadata else sigmas.numel() - 1
block_size = int(metadata.get(METADATA_BLOCK, 1))
if num_steps < 1 or block_size < 1 or num_steps % block_size != 0:
raise ValueError(f'grid={num_steps} block={block_size}')
if sigmas is not None and sigmas.numel() != num_steps + 1:
raise ValueError(f'grid={num_steps} sigmas={sigmas.numel()}')
return num_steps, block_size
def adapter_key(key):
"""True for keys of an adapter family (LoRA factors, alphas, LyCORIS tensors) rather than plain parameters."""
from modules.lora import native_adapter
markers = (native_adapter.LORA_MARKERS + native_adapter.LOKR_MARKERS + native_adapter.LOHA_MARKERS + native_adapter.OFT_MARKERS
+ native_adapter.IA3_MARKERS + native_adapter.GLORA_MARKERS + native_adapter.NORM_MARKERS + native_adapter.FULL_MARKERS + ('.alpha', '.dora_scale'))
return any(marker in key for marker in markers)
def load(name, metadata, state_dict):
"""Collect the per-interval head tensors of a file, or None when the file carries no PDD grid."""
"""Collect the per-interval heads and the replaced parameters of a file, or None when the file carries no PDD grid."""
try:
grid = detect(metadata)
except ValueError as e:
log.error(f'Network load: type=PDD name="{name}" {e} block size must divide the grid')
sigmas = grid_sigmas(metadata)
except (TypeError, ValueError) as e:
log.error(f'Network load: type=PDD name="{name}" {e} unusable grid')
return None
if grid is None:
return None
@@ -87,34 +122,68 @@ def load(name, metadata, state_dict):
if len(heads) == 0:
log.error(f'Network load: type=PDD name="{name}" grid={num_steps} block={block_size} no head tensors')
return None
log.debug(f'Network load: type=PDD name="{name}" grid={num_steps} block={block_size} nfe={num_steps // block_size} heads={list(heads)}')
return ParallelHeads(num_steps, block_size, heads)
head_keys = {f'{path}.{leaf}' for path in heads for leaf in ('weight', 'bias')}
replaced = {key: tensor for key, tensor in state_dict.items() if key not in head_keys and key.rsplit('.', 1)[-1] in ('weight', 'bias') and not adapter_key(key)}
log.debug(f'Network load: type=PDD name="{name}" grid={num_steps} block={block_size} nfe={num_steps // block_size} heads={list(heads)} sigmas={None if sigmas is None else sigmas.tolist()} replaced={len(replaced)}')
return ParallelHeads(num_steps, block_size, heads, sigmas=sigmas, replaced=replaced)
def has_grid(metadata):
return METADATA_STEPS in metadata or METADATA_SIGMAS in metadata
def header_metadata(filename, name):
"""The metadata read from the file itself, for when the cached metadata lacks the grid; warns when head-shaped tensors have no grid."""
"""The metadata read from the file itself, for when the cached metadata lacks the grid."""
from safetensors import safe_open
try:
with safe_open(filename, framework='pt', device='cpu') as f:
metadata = f.metadata() or {}
if METADATA_STEPS in metadata:
if has_grid(metadata):
log.debug(f'Network load: type=PDD name="{name}" grid read from the file header')
else:
heads = sum(1 for key in f.keys() if key.endswith('.weight') and len(f.get_slice(key).get_shape()) == 3)
if heads > 0:
log.warning(f'Network load: type=PDD name="{name}" heads={heads} no {METADATA_STEPS} metadata: heads ignored')
return metadata
except Exception as e:
log.warning(f'Network load: type=PDD name="{name}" header {e}')
return {}
def sidecar_metadata(filename, name):
"""The grid keys of the ``pdd_config.json`` a release keeps beside its weights, as metadata strings; {} without one."""
path = os.path.join(os.path.dirname(filename), SIDECAR)
if not os.path.isfile(path):
return {}
try:
with open(path, encoding='utf8') as f:
config = json.load(f)
except (OSError, ValueError) as e:
log.warning(f'Network load: type=PDD name="{name}" config="{path}" {e}')
return {}
metadata = {key: json.dumps(value) if isinstance(value, list) else str(value) for key, value in config.items() if key in GRID_KEYS}
if has_grid(metadata):
log.debug(f'Network load: type=PDD name="{name}" grid read from "{path}"')
return metadata
def headless_warning(filename, name):
"""Warn when a file holds head-shaped tensors but no grid was found anywhere."""
from safetensors import safe_open
try:
with safe_open(filename, framework='pt', device='cpu') as f:
heads = sum(1 for key in f.keys() if key.endswith('.weight') and len(f.get_slice(key).get_shape()) == 3)
except Exception:
return
if heads > 0:
log.warning(f'Network load: type=PDD name="{name}" heads={heads} no {METADATA_STEPS} or {METADATA_SIGMAS} metadata and no {SIDECAR} beside the file: heads ignored')
def try_load(name, network_on_disk, lora_scale): # pylint: disable=unused-argument
"""Family loader for the native chain: a network carrying only the heads."""
metadata = getattr(network_on_disk, 'metadata', None) or {}
if METADATA_STEPS not in metadata:
if not has_grid(metadata):
metadata = header_metadata(network_on_disk.filename, name) # the metadata cache keeps a failed read forever and --no-metadata returns nothing
if METADATA_STEPS not in metadata:
if not has_grid(metadata):
metadata = {**metadata, **sidecar_metadata(network_on_disk.filename, name)}
if not has_grid(metadata):
headless_warning(network_on_disk.filename, name)
return None
from modules.lora import native_adapter
state_dict = native_adapter.read_state_dict(network_on_disk.filename, what='network')
@@ -194,8 +263,10 @@ class ParallelHead(torch.nn.Module):
return torch.nn.functional.linear(hidden_states, weight, bias)
def grid_intervals(scheduler, num_steps, spec):
"""Interval lengths of the training grid in ascending time, from a pristine scheduler copy immune to a live shift override."""
def grid_intervals(scheduler, num_steps, spec, sigmas=None):
"""Interval lengths of the training grid in ascending time: from the file's own grid, or a pristine scheduler copy immune to a live shift override."""
if sigmas is not None:
return (1.0 - sigmas).diff()
probe = scheduler.__class__.from_config(scheduler.config) if hasattr(scheduler, 'from_config') else copy.deepcopy(scheduler)
probe.set_timesteps(spec.steps_for(num_steps))
sigmas = probe.sigmas.detach().to(device='cpu', dtype=torch.float64)
@@ -228,8 +299,37 @@ def target_shape(module):
return tuple(weight.shape) if weight is not None else None
def undo(modules, replaced):
"""Put back swapped projections and replaced parameter values."""
for parent, attr, original in modules.values():
setattr(parent, attr, original)
for parameter, original in replaced.values():
parameter.data = original
def replace_parameters(net, heads, component, strength):
"""Set every parameter the file replaces, blended toward the loaded value by strength; None on a missing or misshapen target."""
replaced = {}
for path, value in heads.replaced.items():
try:
parameter = component.get_parameter(path)
except AttributeError:
parameter = None
if parameter is None or tuple(parameter.shape) != tuple(value.shape):
log.error(f'Network load: type=PDD name="{net.name}" parameter={path} shape={list(value.shape)} module={None if parameter is None else list(parameter.shape)} no matching parameter')
undo({}, replaced)
return None
original = parameter.data
target = value.to(device=original.device, dtype=torch.float32)
if strength != 1.0:
target = original.to(dtype=torch.float32) + strength * (target - original.to(dtype=torch.float32))
parameter.data = target.to(dtype=original.dtype)
replaced[path] = (parameter, original)
return replaced
def install(pipe, net, heads, spec, components):
"""Swap the heads into the component that owns their projections; True when the module tree changed."""
"""Swap the heads into the component that owns their projections and set the parameters the file replaces; True when the module tree changed."""
component_name, component = owner(pipe, heads, components)
if component is None:
log.error(f'Network load: type=PDD name="{net.name}" heads={list(heads.heads)} no loaded component holds these projections')
@@ -242,16 +342,14 @@ def install(pipe, net, heads, spec, components):
shape = target_shape(module)
if shape != tuple(weight.shape[1:]):
log.error(f'Network load: type=PDD name="{net.name}" head={path} shape={list(weight.shape[1:])} module={list(shape) if shape else None} shape mismatch')
for parent, attr, original in modules.values():
setattr(parent, attr, original)
undo(modules, {})
return False
scheduler_name = spec.scheduler_name(path)
scheduler = getattr(pipe, scheduler_name, None)
intervals = grid_intervals(scheduler, heads.num_steps, spec) if scheduler is not None else None
intervals = grid_intervals(scheduler, heads.num_steps, spec, heads.sigmas) if scheduler is not None or heads.sigmas is not None else None
if intervals is None:
log.error(f'Network load: type=PDD name="{net.name}" head={path} scheduler={scheduler.__class__.__name__} cannot build a {heads.num_steps}-interval grid')
for parent, attr, original in modules.values():
setattr(parent, attr, original)
undo(modules, {})
return False
def get_scheduler(name=scheduler_name):
owner_pipe = pipe_ref()
@@ -263,20 +361,23 @@ def install(pipe, net, heads, spec, components):
parent = component.get_submodule(parent_path) if parent_path else component
setattr(parent, attr, head)
modules[path] = (parent, attr, module)
pipe.sdnext_pdd = Installed(net.name, strength, component_name, modules, heads, spec)
log.info(f'Network load: type=PDD name="{net.name}" component={component_name} heads={list(modules)} grid={heads.num_steps} block={heads.block_size} nfe={heads.nfe} steps={pipe.sdnext_pdd.steps} strength={strength}')
replaced = replace_parameters(net, heads, component, strength)
if replaced is None:
undo(modules, {})
return False
pipe.sdnext_pdd = Installed(net.name, strength, component_name, modules, heads, spec, replaced)
log.info(f'Network load: type=PDD name="{net.name}" component={component_name} heads={list(modules)} grid={heads.num_steps} block={heads.block_size} nfe={heads.nfe} steps={pipe.sdnext_pdd.steps} replaced={len(replaced)} strength={strength}')
return True
def restore(pipe):
"""Put the original projections back; True when heads were installed."""
"""Put the original projections and parameter values back; True when heads were installed."""
state = getattr(pipe, 'sdnext_pdd', None)
if state is None:
return False
for parent, attr, original in state.modules.values():
setattr(parent, attr, original)
undo(state.modules, state.replaced)
del pipe.sdnext_pdd
log.info(f'Network unload: type=PDD name="{state.name}" component={state.component} heads={list(state.modules)}')
log.info(f'Network unload: type=PDD name="{state.name}" component={state.component} heads={list(state.modules)} replaced={len(state.replaced)}')
return True
@@ -311,26 +412,54 @@ def reconcile(pipe, loaded, components):
return install(pipe, net, net.extras[EXTRAS_KEY], spec, components) or changed
def settable(obj, name):
"""False for a property without a setter."""
attribute = getattr(type(obj), name, None)
return not isinstance(attribute, property) or attribute.fset is not None
SIGMA_TRANSFORMS = {'use_dynamic_shifting': False, 'shift_terminal': None, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False, 'invert_sigmas': False}
def take_sigmas_as_given(scheduler):
"""Turn off every transform a flow scheduler applies to explicit sigmas; the sampler builds a fresh scheduler for each generation."""
config = getattr(scheduler, 'config', None) or {}
flags = {key: value for key, value in SIGMA_TRANSFORMS.items() if key in config}
if flags and hasattr(scheduler, 'register_to_config'):
scheduler.register_to_config(**flags)
if hasattr(scheduler, 'set_shift'):
scheduler.set_shift(1.0)
def pin(p, model):
"""Hold a generation on the distilled evaluation count and shipped schedule while heads are installed; returns the scheduler step argument or None."""
state = getattr(model, 'sdnext_pdd', None)
if state is None:
return None
shifts = {}
for path in state.modules:
name = state.spec.scheduler_name(path)
scheduler = getattr(model, name, None)
if scheduler is not None and hasattr(scheduler, 'set_shift') and getattr(scheduler, 'config', None) is not None and 'shift' in scheduler.config:
scheduler.set_shift(scheduler.config['shift'])
shifts[name] = scheduler.config['shift']
sigmas = None
if state.heads.sigmas is not None:
scheduler = getattr(model, state.spec.default_scheduler, None)
if scheduler is not None:
take_sigmas_as_given(scheduler)
sigmas = state.heads.sigmas[::state.heads.block_size][:-1].tolist() # block boundaries; the scheduler appends the terminal zero
if getattr(p, 'task_args', None) is not None:
p.task_args['sigmas'] = sigmas
else:
for path in state.modules:
name = state.spec.scheduler_name(path)
scheduler = getattr(model, name, None)
if scheduler is not None and hasattr(scheduler, 'set_shift') and getattr(scheduler, 'config', None) is not None and 'shift' in scheduler.config:
scheduler.set_shift(scheduler.config['shift'])
shifts[name] = scheduler.config['shift']
requested = p.steps
p.steps = state.heads.nfe # what the user-facing step count means: transformer evaluations
if getattr(p, 'task_args', None) is not None:
p.task_args['num_inference_steps'] = state.steps # the scheduler argument that yields that many grid intervals
if getattr(model, 'num_timesteps', None) is not None:
model.num_timesteps = state.heads.nfe # the progress total counts transformer evaluations
if getattr(model, 'num_timesteps', None) is not None and settable(model, 'num_timesteps'):
model.num_timesteps = state.heads.nfe # the progress total counts transformer evaluations; a read-only property is the pipeline counting its own timesteps
extra = getattr(p, 'extra_generation_params', None)
if extra is not None:
extra.update({state.spec.shift_keys[name]: shift for name, shift in shifts.items() if name in state.spec.shift_keys})
log.info(f'Network: type=PDD name="{state.name}" steps={state.heads.nfe} requested={requested} grid_steps={state.steps} shift={shifts}')
log.info(f'Network: type=PDD name="{state.name}" steps={state.heads.nfe} requested={requested} grid_steps={state.steps} shift={shifts} sigmas={sigmas}')
return state.steps
+9 -1
View File
@@ -15,12 +15,19 @@ under every prefix. ComfyUI checkpoints fuse the SwiGLU input as
``img_mlp.gate_up`` with rows ``[gate_layer; proj]``, and so do the LoRAs
ai-toolkit and musubi-tuner train against them; :func:`resolve_targets` splits
that projection into the two diffusers modules.
Parallel decoding exports (alibaba-pai Fun-Acc) carry their own sigma grid, so
:data:`PDD` needs no scheduler mapping: the pin hands the pipeline that grid.
"""
from modules.lora import native_adapter
from modules.lora import native_adapter, network_pdd
from modules.lora.native_adapter import ChunkSpec
# One output projection on the pipeline scheduler; with explicit sigmas the step count is the interval count.
PDD = network_pdd.ArchSpec()
# === Arch-specific prefix configuration ===
KNOWN_PREFIXES = native_adapter.KNOWN_PREFIXES_DEFAULT
@@ -142,5 +149,6 @@ def try_load(name, network_on_disk, lora_scale):
family_loaders=(
try_load_lora, try_load_lokr, try_load_loha, try_load_oft,
try_load_ia3, try_load_glora, try_load_norm, try_load_full,
network_pdd.try_load,
),
)
+152
View File
@@ -19,6 +19,7 @@ Usage:
import os
import sys
import json
import types
import torch
@@ -46,6 +47,7 @@ from modules import shared # pylint: disable=wrong-import-position,unused-impor
from modules.errors import log # pylint: disable=wrong-import-position
from modules.lora import network_pdd # pylint: disable=wrong-import-position
from pipelines.minimax import minimax_lora # pylint: disable=wrong-import-position
from pipelines.qwen import qwen21_lora # pylint: disable=wrong-import-position
NUM_STEPS = 32
@@ -152,6 +154,45 @@ def make_net(heads, strength=1.0):
return types.SimpleNamespace(name='pdd-test', te_multiplier=strength, extras={network_pdd.EXTRAS_KEY: heads})
# Qwen-Image-2.1-Fun-Acc-4Step: an inference-only export with one head per step, its own sigma grid and replaced norm weights.
QWEN_SIGMAS = [1.0, 0.9169867038726807, 0.7861579060554504, 0.5494909882545471, 0.0]
QWEN_SCHEDULER = dict(use_dynamic_shifting=True, base_shift=0.5, max_shift=0.9, base_image_seq_len=256, max_image_seq_len=8192, shift_terminal=0.02, time_shift_type='exponential')
class StubQwenPipe:
"""A single-stream model: one bias-free output projection, a norm the export replaces, a dynamic-shift flow scheduler,
and the read-only num_timesteps property diffusers pipelines expose."""
def __init__(self):
self.transformer = torch.nn.Module()
self.transformer.proj_out = torch.nn.Linear(HIDDEN, VIDEO_OUT, bias=False)
self.transformer.block = torch.nn.Module()
self.transformer.block.norm_q = torch.nn.RMSNorm(8)
self.scheduler = diffusers.FlowMatchEulerDiscreteScheduler(**QWEN_SCHEDULER)
self._num_timesteps = 40
@property
def num_timesteps(self):
return self._num_timesteps
def qwen_state_dict():
torch.manual_seed(1)
return {
'proj_out.weight': torch.randn(4, VIDEO_OUT, HIDDEN),
'block.norm_q.weight': torch.full((8,), 2.0),
'block.attn.to_q.lora_down': torch.zeros(4, HIDDEN),
'block.attn.to_q.lora_up': torch.zeros(HIDDEN, 4),
'block.attn.to_k.lora_A.weight': torch.zeros(4, HIDDEN),
'block.attn.to_k.lora_B.weight': torch.zeros(HIDDEN, 4),
'block.attn.to_k.alpha': torch.tensor(4.0),
}
def qwen_heads():
return network_pdd.load('acc', {'pdd_sigmas': json.dumps(QWEN_SIGMAS)}, qwen_state_dict())
# ============================================================
# Tests: detection and loading
# ============================================================
@@ -369,6 +410,113 @@ def test_pin_overrides_steps_and_shift():
assert network_pdd.pin(p, pipe) is None
# ============================================================
# Tests: explicit grids and replaced parameters
# ============================================================
def test_detect_grid_from_sigmas():
metadata = {'pdd_sigmas': json.dumps(QWEN_SIGMAS)}
assert network_pdd.detect(metadata) == (4, 1)
sigmas = network_pdd.grid_sigmas(metadata)
assert sigmas.dtype == torch.float64 and sigmas.tolist() == QWEN_SIGMAS
assert network_pdd.detect({'pdd_num_steps': '4', 'pdd_sigmas': json.dumps(QWEN_SIGMAS)}) == (4, 1)
def test_detect_rejects_bad_sigmas():
for sigmas in ([1.0, 0.5], [0.9, 0.5, 0.0], [1.0, 0.5, 0.6, 0.0]):
try:
network_pdd.detect({'pdd_sigmas': json.dumps(sigmas)})
except ValueError:
continue
raise AssertionError(f'sigmas {sigmas} accepted')
try:
network_pdd.detect({'pdd_num_steps': '8', 'pdd_sigmas': json.dumps(QWEN_SIGMAS)})
except ValueError:
return True
raise AssertionError('an 8-step grid accepted 5 sigmas')
def test_load_collects_replaced_parameters():
heads = qwen_heads()
assert heads is not None and set(heads.heads) == {'proj_out'} and heads.heads['proj_out'][1] is None
assert set(heads.replaced) == {'block.norm_q.weight'}, f'replaced={list(heads.replaced)}'
assert heads.nfe == 4 and heads.sigmas.tolist() == QWEN_SIGMAS
def test_try_load_reads_the_sidecar_config():
import tempfile
from safetensors.torch import save_file
with tempfile.TemporaryDirectory() as folder:
path = os.path.join(folder, 'acc.safetensors')
save_file(qwen_state_dict(), path, metadata={'format': 'qwenimage21_extracted_prefused_v1'})
disk = types.SimpleNamespace(name='acc', filename=path, metadata={})
assert network_pdd.try_load('acc', disk, 1.0) is None, 'no grid anywhere must leave the heads out'
with open(os.path.join(folder, network_pdd.SIDECAR), 'w', encoding='utf8') as f:
json.dump({'pdd_num_steps': 4, 'pdd_block_size': 1, 'pdd_sigmas': QWEN_SIGMAS, 'lora_rank': 64}, f)
net = network_pdd.try_load('acc', disk, 1.0)
assert net is not None, 'the sidecar grid must be enough'
heads = net.extras[network_pdd.EXTRAS_KEY]
assert heads.nfe == 4 and heads.sigmas.tolist() == QWEN_SIGMAS
def test_install_replaces_and_restores_parameters():
pipe = StubQwenPipe()
original = pipe.transformer.block.norm_q.weight.detach().clone()
heads = qwen_heads()
assert network_pdd.install(pipe, make_net(heads), heads, qwen21_lora.PDD, ['transformer']) is True
assert torch.equal(pipe.transformer.block.norm_q.weight, torch.full((8,), 2.0))
network_pdd.restore(pipe)
assert torch.equal(pipe.transformer.block.norm_q.weight, original)
assert network_pdd.install(pipe, make_net(heads, strength=0.5), heads, qwen21_lora.PDD, ['transformer']) is True
assert torch.allclose(pipe.transformer.block.norm_q.weight, original + 0.5 * (2.0 - original))
network_pdd.restore(pipe)
assert torch.equal(pipe.transformer.block.norm_q.weight, original)
def test_install_refuses_misshapen_replacement():
pipe = StubQwenPipe()
original_head = pipe.transformer.proj_out
heads = qwen_heads()
heads.replaced['block.norm_q.weight'] = torch.ones(16)
assert network_pdd.install(pipe, make_net(heads), heads, qwen21_lora.PDD, ['transformer']) is False
assert pipe.transformer.proj_out is original_head and not hasattr(pipe, 'sdnext_pdd')
def test_one_head_per_step_on_a_block1_grid():
pipe = StubQwenPipe()
heads = qwen_heads()
assert network_pdd.install(pipe, make_net(heads), heads, qwen21_lora.PDD, ['transformer']) is True
x = torch.randn(3, HIDDEN)
for index in range(4):
pipe.scheduler._step_index = index # pylint: disable=protected-access
expected = torch.nn.functional.linear(x, heads.heads['proj_out'][0][index])
assert torch.allclose(pipe.transformer.proj_out(x), expected, atol=1e-6), f'step {index} did not select its own head'
network_pdd.restore(pipe)
def test_pin_hands_the_exact_grid():
pipe = StubQwenPipe()
try:
pipe.num_timesteps = 4
except AttributeError:
pass
else:
raise AssertionError('the stub accepts num_timesteps assignment; it does not model the diffusers property')
heads = qwen_heads()
assert network_pdd.install(pipe, make_net(heads), heads, qwen21_lora.PDD, ['transformer']) is True
p = types.SimpleNamespace(steps=40, task_args={}, extra_generation_params={})
assert network_pdd.pin(p, pipe) == 4 and p.steps == 4 and p.task_args['num_inference_steps'] == 4
assert pipe.num_timesteps == 40, 'the pipeline counts its own timesteps'
assert p.task_args['sigmas'] == QWEN_SIGMAS[:-1]
pipe.scheduler.set_timesteps(sigmas=p.task_args['sigmas'], mu=1.31) # the pipeline passes a resolution-dependent mu
grid = torch.tensor(QWEN_SIGMAS, dtype=torch.float64)
assert torch.allclose(pipe.scheduler.sigmas.double(), grid, atol=1e-6), f'sigmas={pipe.scheduler.sigmas.tolist()}'
unpinned = diffusers.FlowMatchEulerDiscreteScheduler(**QWEN_SCHEDULER)
unpinned.set_timesteps(sigmas=p.task_args['sigmas'], mu=1.31)
assert not torch.allclose(unpinned.sigmas.double(), grid, atol=1e-3), 'the unpinned scheduler also lands on the grid; the check does not discriminate'
network_pdd.restore(pipe)
# ============================================================
# Main
# ============================================================
@@ -383,6 +531,10 @@ def main():
cat = category('lifecycle')
for fn in (test_install_and_restore_round_trip, test_install_refuses_shape_mismatch, test_install_needs_an_owner, test_reconcile_follows_loaded_set, test_pin_overrides_steps_and_shift):
run_test(cat, fn)
cat = category('grid')
for fn in (test_detect_grid_from_sigmas, test_detect_rejects_bad_sigmas, test_load_collects_replaced_parameters, test_try_load_reads_the_sidecar_config,
test_install_replaces_and_restores_parameters, test_install_refuses_misshapen_replacement, test_one_head_per_step_on_a_block1_grid, test_pin_hands_the_exact_grid):
run_test(cat, fn)
failed = sum(r['failed'] for r in results.values())
passed = sum(r['passed'] for r in results.values())
log.info(f'PDD tests: passed={passed} failed={failed}')