221 lines
7.1 KiB
Python
221 lines
7.1 KiB
Python
import gc
|
|
import os
|
|
import torch
|
|
from pathlib import Path
|
|
|
|
import folder_paths
|
|
|
|
NODE_DIR = Path(__file__).parent
|
|
CONFIGS_DIR = NODE_DIR / "walkyrie_configs"
|
|
|
|
_pipe_cache = {}
|
|
|
|
|
|
def _split_state_dict(merged_sd):
|
|
components = {"transformer": {}, "text_encoder": {}, "vae": {}}
|
|
for k, v in merged_sd.items():
|
|
for comp in components:
|
|
if k.startswith(comp + "."):
|
|
components[comp][k[len(comp) + 1:]] = v
|
|
break
|
|
return components
|
|
|
|
|
|
def _unload_cached_pipe():
|
|
global _pipe_cache
|
|
if not _pipe_cache:
|
|
return
|
|
print("[Walkyrie] Clearing previous pipe from memory...")
|
|
for pipe in _pipe_cache.values():
|
|
pipe.to("cpu")
|
|
del pipe
|
|
_pipe_cache.clear()
|
|
gc.collect()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
print("[Walkyrie] Memory cleared.")
|
|
|
|
|
|
def _load_pipe(ckpt_path, torch_dtype):
|
|
"""
|
|
Build a pipeline_walkyrie instance from a merged safetensors checkpoint.
|
|
Mirrors what merge_to_single.py produces:
|
|
transformer.* / text_encoder.* / vae.*
|
|
"""
|
|
from safetensors.torch import load_file
|
|
from transformers import AutoTokenizer, UMT5EncoderModel, AutoConfig
|
|
from diffusers import (
|
|
AutoencoderKLWan,
|
|
WanTransformer3DModel,
|
|
FlowMatchEulerDiscreteScheduler,
|
|
)
|
|
import sys, pathlib
|
|
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
|
from pipeline_walkyrie import pipeline_walkyrie
|
|
|
|
cache_key = (ckpt_path, torch_dtype)
|
|
if cache_key in _pipe_cache:
|
|
print("[Walkyrie] Using cached pipe.")
|
|
return _pipe_cache[cache_key]
|
|
|
|
if _pipe_cache:
|
|
_unload_cached_pipe()
|
|
|
|
configs = str(CONFIGS_DIR)
|
|
print(f"[Walkyrie] Loading checkpoint: {ckpt_path}")
|
|
merged_sd = load_file(ckpt_path, device="cpu")
|
|
sd = _split_state_dict(merged_sd)
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained(configs, subfolder="tokenizer")
|
|
|
|
te_config = AutoConfig.from_pretrained(os.path.join(configs, "text_encoder"))
|
|
text_encoder = UMT5EncoderModel(te_config).to(torch_dtype)
|
|
text_encoder.load_state_dict(sd["text_encoder"], strict=False)
|
|
text_encoder.eval()
|
|
|
|
vae_config = AutoencoderKLWan.load_config(configs, subfolder="vae")
|
|
vae = AutoencoderKLWan.from_config(vae_config).to(torch_dtype)
|
|
vae.load_state_dict(sd["vae"], strict=False)
|
|
vae.eval()
|
|
|
|
tr_config = WanTransformer3DModel.load_config(configs, subfolder="transformer")
|
|
transformer = WanTransformer3DModel.from_config(tr_config).to(torch_dtype)
|
|
transformer.load_state_dict(sd["transformer"], strict=False)
|
|
transformer.eval()
|
|
|
|
scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(configs, subfolder="scheduler")
|
|
|
|
pipe = pipeline_walkyrie(
|
|
tokenizer = tokenizer,
|
|
text_encoder = text_encoder,
|
|
vae = vae,
|
|
transformer = transformer,
|
|
scheduler = scheduler,
|
|
)
|
|
|
|
_pipe_cache[cache_key] = pipe
|
|
print("[Walkyrie] Pipe ready.")
|
|
return pipe
|
|
|
|
|
|
class WalkyrieLoader:
|
|
"""Loads a merged Walkyrie safetensors checkpoint into a pipeline."""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
|
|
"dtype" : (["bfloat16", "float16", "float32"], {"default": "bfloat16"}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("WALKYRIE_PIPE",)
|
|
RETURN_NAMES = ("pipe",)
|
|
FUNCTION = "load"
|
|
CATEGORY = "Walkyrie"
|
|
|
|
def load(self, ckpt_name, dtype):
|
|
dtype_map = {"bfloat16": torch.bfloat16, "float16": torch.float16, "float32": torch.float32}
|
|
torch_dtype = dtype_map[dtype]
|
|
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
|
pipe = _load_pipe(ckpt_path, torch_dtype)
|
|
return (pipe,)
|
|
|
|
|
|
class WalkyrieLoraLoader:
|
|
"""
|
|
Calls pipe.load_lora_weights() then pipe.fuse_lora() —
|
|
identical to the reference usage:
|
|
pipe.load_lora_weights("lora.safetensors")
|
|
pipe.fuse_lora(lora_scale=1.0)
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipe" : ("WALKYRIE_PIPE",),
|
|
"lora_name" : (folder_paths.get_filename_list("loras"),),
|
|
"lora_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("WALKYRIE_PIPE",)
|
|
RETURN_NAMES = ("pipe",)
|
|
FUNCTION = "load_lora"
|
|
CATEGORY = "Walkyrie"
|
|
|
|
def load_lora(self, pipe, lora_name, lora_scale):
|
|
lora_path = folder_paths.get_full_path("loras", lora_name)
|
|
print(f"[Walkyrie] Loading LoRA: {lora_path} (scale={lora_scale})")
|
|
pipe.load_lora_weights(lora_path)
|
|
pipe.fuse_lora(lora_scale=lora_scale)
|
|
print("[Walkyrie] LoRA fused.")
|
|
return (pipe,)
|
|
|
|
|
|
class WalkyrieSampler:
|
|
"""Runs pipe() directly. Output is converted to ComfyUI IMAGE format."""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipe" : ("WALKYRIE_PIPE",),
|
|
"prompt" : ("STRING", {"multiline": True, "default": ""}),
|
|
"height" : ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 16}),
|
|
"width" : ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 16}),
|
|
"steps" : ("INT", {"default": 20, "min": 1, "max": 200}),
|
|
"guidance_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 20.0, "step": 0.1}),
|
|
"seed" : ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "sample"
|
|
CATEGORY = "Walkyrie"
|
|
|
|
def sample(self, pipe, prompt, height, width, steps, guidance_scale, seed):
|
|
from comfy.utils import ProgressBar
|
|
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
pipe.to(device)
|
|
generator = torch.Generator(device=device).manual_seed(seed)
|
|
|
|
comfy_pbar = ProgressBar(steps)
|
|
|
|
def _step_callback(pipeline, i, t, callback_kwargs):
|
|
comfy_pbar.update(1)
|
|
return callback_kwargs
|
|
|
|
output = pipe(
|
|
prompt = prompt,
|
|
height = height,
|
|
width = width,
|
|
num_inference_steps = steps,
|
|
guidance_scale = guidance_scale,
|
|
generator = generator,
|
|
output_type = "np",
|
|
callback_on_step_end = _step_callback,
|
|
)
|
|
|
|
import numpy as np
|
|
frames = output.frames
|
|
image = torch.from_numpy(np.stack(frames))
|
|
return (image,)
|
|
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"WalkyrieLoader" : WalkyrieLoader,
|
|
"WalkyrieLoraLoader": WalkyrieLoraLoader,
|
|
"WalkyrieSampler" : WalkyrieSampler,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"WalkyrieLoader" : "Walkyrie Loader",
|
|
"WalkyrieLoraLoader": "Walkyrie LoRA Loader",
|
|
"WalkyrieSampler" : "Walkyrie Sampler",
|
|
}
|