Files
WJLUOXIAO-XB_ToolBox/nodes_vis.py
T
2026-06-25 04:50:33 +08:00

316 lines
16 KiB
Python

import torch
import numpy as np
import os
from PIL import Image, ImageDraw, ImageFont
class CalculatedFloat(float):
pass
# ============================================================
# XB_VRAM_Calculator — 显存计算器
# ============================================================
class XB_VRAM_Calculator:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Total_VRAM_GB": ("INT", {"default": 24, "min": 4, "max": 128, "step": 1, "display": "number"}),
"System_Overhead_GB": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 64.0, "step": 0.5, "display": "number"}),
"Main_Video_Model": (["LTX-2.3 (22B)", "WAN2.2-I2V (14B)", "WAN2.2-T2V (14B)", "WAN2.2-Animate (14B)"], {"default": "LTX-2.3 (22B)"}),
"Model_Quantization": (["FP16/BF16", "FP8", "GGUF-Q8", "GGUF-Q6", "GGUF-Q4"], {"default": "FP8"}),
"Total_Model_Layers": ("INT", {"default": 48, "min": 1, "max": 200, "step": 1, "display": "number"}),
"Layers_to_Swap": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1, "display": "number"}),
"LoRA_Plugin_VRAM_GB": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 64.0, "step": 0.1, "display": "number"}),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("Available_VRAM",)
FUNCTION = "calculate_vram"
CATEGORY = "XB_ToolBox/Chunk_Preview"
def calculate_vram(self, Total_VRAM_GB, System_Overhead_GB, Main_Video_Model, Model_Quantization, Total_Model_Layers, Layers_to_Swap, LoRA_Plugin_VRAM_GB):
vram_map = {
"LTX-2.3 (22B)": {"FP16/BF16": 44.0, "FP8": 22.5, "GGUF-Q8": 22.8, "GGUF-Q6": 17.8, "GGUF-Q4": 13.5},
"WAN2.2-I2V (14B)": {"FP16/BF16": 28.0, "FP8": 14.0, "GGUF-Q8": 14.5, "GGUF-Q6": 11.0, "GGUF-Q4": 9.0},
"WAN2.2-T2V (14B)": {"FP16/BF16": 28.0, "FP8": 14.0, "GGUF-Q8": 14.5, "GGUF-Q6": 11.0, "GGUF-Q4": 9.0},
"WAN2.2-Animate (14B)": {"FP16/BF16": 28.0, "FP8": 14.0, "GGUF-Q8": 14.5, "GGUF-Q6": 11.0, "GGUF-Q4": 9.0},
}
full_model_size = vram_map.get(Main_Video_Model, {}).get(Model_Quantization, 14.0)
stay_layers = max(0, Total_Model_Layers - Layers_to_Swap)
active_model_vram = (full_model_size / max(1, Total_Model_Layers)) * stay_layers
available_vram = Total_VRAM_GB - System_Overhead_GB - active_model_vram - LoRA_Plugin_VRAM_GB
available_vram = max(0.1, available_vram)
res = CalculatedFloat(available_vram)
res._stay = stay_layers
res._total = Total_Model_Layers
return (res,)
# ============================================================
# XB_ChunkVisualization — 分块可视化
# ============================================================
class XB_ChunkVisualization:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"Stat_Mode": (["Spatial Only", "Temporal Only", "Spatial & Temporal"], {"default": "Spatial & Temporal"}),
"Available_VRAM_GB": ("FLOAT", {"default": 12.0, "min": 0.1, "max": 128.0, "step": 0.1, "display": "number"}),
"Image_Width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16, "display": "number"}),
"Image_Height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16, "display": "number"}),
"Image_Frames": ("INT", {"default": 1, "min": 1, "max": 10000, "step": 4, "display": "number"}),
"Current_Stage": (["Encode Stage", "Decode Stage"], {"default": "Decode Stage"}),
"Spatial_Tile_Size": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 32, "display": "number"}),
"Spatial_Tile_Overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32, "display": "number"}),
"Temporal_Chunk_Size": ("INT", {"default": 33, "min": 8, "max": 10000, "step": 4, "display": "number"}),
"Temporal_Chunk_Overlap": ("INT", {"default": 4, "min": 0, "max": 1000, "step": 4, "display": "number"}),
},
"optional": {
"Image_Input": ("IMAGE",)
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("preview_image",)
FUNCTION = "visualize_chunks"
CATEGORY = "XB_ToolBox"
def get_1d_chunks(self, total_len, chunk_size, overlap):
chunks = []
if chunk_size <= 0: return [(0, total_len)]
stride = max(1, chunk_size - min(overlap, int(chunk_size * 0.9)))
curr = 0
while curr < total_len:
start, end = curr, min(curr + chunk_size, total_len)
chunks.append((start, end))
if end >= total_len: break
curr += stride
return chunks
def visualize_chunks(self, Stat_Mode, Available_VRAM_GB, Image_Width, Image_Height, Image_Frames, Current_Stage,
Spatial_Tile_Size, Spatial_Tile_Overlap, Temporal_Chunk_Size, Temporal_Chunk_Overlap, Image_Input=None):
active_vram = float(Available_VRAM_GB)
is_calc = type(Available_VRAM_GB).__name__ == "CalculatedFloat"
stay_info = f" (Reside:{Available_VRAM_GB._stay}/{Available_VRAM_GB._total}L)" if (is_calc and hasattr(Available_VRAM_GB, '_stay')) else ""
if Image_Input is not None:
B, H_orig, W_orig, C = Image_Input.shape
source_img = Image.fromarray((Image_Input[0].cpu().numpy() * 255).astype(np.uint8)).convert("RGBA")
res_info = f"[Size]: {W_orig}x{H_orig} (Input)"
else:
W_orig, H_orig = Image_Width, Image_Height
source_img = Image.new("RGBA", (W_orig, H_orig), (25, 25, 25, 255))
res_info = f"[Size]: {W_orig}x{H_orig} (Param)"
x_chunks = self.get_1d_chunks(W_orig, Spatial_Tile_Size if "Spatial" in Stat_Mode else 0, Spatial_Tile_Overlap)
y_chunks = self.get_1d_chunks(H_orig, Spatial_Tile_Size if "Spatial" in Stat_Mode else 0, Spatial_Tile_Overlap)
t_chunks = self.get_1d_chunks(Image_Frames, Temporal_Chunk_Size if "Temporal" in Stat_Mode else 0, Temporal_Chunk_Overlap)
total_batches = len(x_chunks) * len(y_chunks) * len(t_chunks)
base_vol = W_orig * H_orig * Image_Frames
real_vol, max_chunk_vol = 0, 0
for x1, x2 in x_chunks:
for y1, y2 in y_chunks:
for t1, t2 in t_chunks:
v = (x2-x1)*(y2-y1)*(t2-t1)
real_vol += v
max_chunk_vol = max(max_chunk_vol, v)
redundancy = ((real_vol - base_vol) / base_vol * 100) if base_vol > 0 else 0
safe_unit = active_vram * 5_000_000
kappa = (max_chunk_vol / max(1, safe_unit)) ** 1.15
p_percent = min(100, (kappa / 2.0) * 100)
p_color = (0, 255, 0) if p_percent < 60 else ((255, 200, 0) if p_percent < 85 else (255, 50, 50))
# 🛡️ 全局字体缓存:避免 Auto-Queue 模式下反复磁盘 I/O 和内存泄漏
_FONT_CACHE = {}
def get_font(size):
font_paths = [
# Windows
os.path.join(os.environ.get('WINDIR', 'C:/Windows'), "Fonts", "msyh.ttc"),
os.path.join(os.environ.get('WINDIR', 'C:/Windows'), "Fonts", "simhei.ttf"),
# Linux
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
# macOS
"/System/Library/Fonts/STHeiti Medium.ttc",
"/System/Library/Fonts/Helvetica.ttc",
]
for p in font_paths:
cache_key = (p, size)
if cache_key in _FONT_CACHE:
return _FONT_CACHE[cache_key]
if os.path.exists(p):
try:
font = ImageFont.truetype(p, size)
_FONT_CACHE[cache_key] = font
return font
except Exception:
continue
# 兜底字体也缓存
if ("_fallback", size) in _FONT_CACHE:
return _FONT_CACHE[("_fallback", size)]
fb = ImageFont.load_default()
_FONT_CACHE[("_fallback", size)] = fb
return fb
f_info = get_font(22)
f_num = get_font(28)
temp_draw = ImageDraw.Draw(Image.new("RGBA", (1,1)))
show_time = ("Temporal" in Stat_Mode and Image_Frames > 1)
sidebar_w = 250 if show_time else 0
gap = 50 if show_time else 0
padding = 70
draw_W = W_orig + gap + sidebar_w
new_W = max(draw_W + 80, 700)
max_txt_w = new_W - 50
info_cats = [
{"text": f"[Mode]: {Stat_Mode} / [Stage]: {Current_Stage}", "color": (0, 255, 255)},
{"text": res_info, "color": (220, 220, 220)},
{"text": f"[Batches]: {total_batches} (S:{len(x_chunks)*len(y_chunks)} x T:{len(t_chunks)})", "color": (255, 255, 255)},
{"text": f"[VRAM]: {active_vram:.1f}G{stay_info}", "color": (150, 200, 255)},
{"text": f"[Volume]: {real_vol/1e6:.1f}M (Base:{base_vol/1e6:.1f}M | Redundancy:+{redundancy:.1f}%)", "color": (255, 150, 200)},
{"text": f"[Chunk]: {max_chunk_vol/1e6:.1f}M/b / [Pressure]: {kappa:.2f}", "color": p_color}
]
final_lines = []
for cat in info_cats:
prefix, content = cat["text"].split("]: ", 1) if "]: " in cat["text"] else ("", cat["text"])
if prefix: prefix += "]: "
prefix_w = int(temp_draw.textlength(prefix, font=f_info))
curr, is_first = "", True
for char in content:
limit = max_txt_w if is_first else (max_txt_w - prefix_w)
if temp_draw.textlength(curr + char, font=f_info) <= limit:
curr += char
else:
final_lines.append((prefix + curr if is_first else curr, cat["color"], 0 if is_first else prefix_w))
curr, is_first = char, False
if curr:
final_lines.append((prefix + curr if is_first else curr, cat["color"], 0 if is_first else prefix_w))
line_h = 36
header_H = len(final_lines) * line_h + 80
paste_x = int((new_W - draw_W) / 2)
paste_y = header_H + padding
full_canvas = Image.new("RGBA", (int(new_W), int(paste_y + H_orig + 40)), (20, 20, 20, 255))
if Stat_Mode != "Temporal Only":
full_canvas.paste(source_img, (paste_x, paste_y))
else:
faded = source_img.copy()
faded.putalpha(60)
full_canvas.paste(faded, (paste_x, paste_y), faded)
draw = ImageDraw.Draw(full_canvas, "RGBA")
y = 15
for txt, clr, x_off in final_lines:
draw.text((25 + x_off, y), txt, fill=clr, font=f_info)
y += line_h
draw.rounded_rectangle([25, y+10, 525, y+22], radius=6, fill=(45, 45, 45))
draw.rounded_rectangle([25, y+10, 25 + int(500 * (p_percent/100)), y+22], radius=6, fill=p_color)
colors = [(0, 255, 0), (0, 255, 255), (255, 255, 0), (255, 0, 255), (0, 150, 255), (255, 120, 0), (255, 50, 50)]
title_y = paste_y - 45
bar_cx = paste_x + W_orig + gap + sidebar_w // 2 - 40
if "Spatial" in Stat_Mode:
draw.text((paste_x + W_orig//2 - 84, title_y), "[Spatial]", fill=(220,220,220,255), font=f_num)
if show_time:
draw.text((bar_cx - 84, title_y), "[Temporal]", fill=(220,220,220,255), font=f_num)
if "Spatial" in Stat_Mode:
spatial_idx = 0
for y1, y2 in y_chunks:
for x1, x2 in x_chunks:
c = colors[spatial_idx % len(colors)]
rect = [paste_x + x1, paste_y + y1, paste_x + x2, paste_y + y2]
draw.rectangle(rect, fill=None, outline=c, width=3)
if total_batches < 150:
lbl_x, lbl_y = paste_x + x1 + 10, paste_y + y1 + 10
draw.text((lbl_x, lbl_y), f"S{spatial_idx+1}", fill=c, font=f_num, stroke_width=3, stroke_fill=(0,0,0,255))
spatial_idx += 1
if show_time:
rx = 60
ry = 18
draw.line([bar_cx, paste_y, bar_cx, paste_y + H_orig], fill=(70, 70, 70, 255), width=2)
scale_y = H_orig / max(1, Image_Frames)
def draw_cylinder(d, cx, cy1, cy2, rx, ry, fill_color, outline_color, line_width):
if fill_color:
d.ellipse([cx - rx, cy2 - ry, cx + rx, cy2 + ry], fill=fill_color, outline=outline_color, width=line_width)
d.rectangle([cx - rx, cy1, cx + rx, cy2], fill=fill_color)
d.line([cx - rx, cy1, cx - rx, cy2], fill=outline_color, width=line_width)
d.line([cx + rx, cy1, cx + rx, cy2], fill=outline_color, width=line_width)
d.ellipse([cx - rx, cy1 - ry, cx + rx, cy1 + ry], fill=fill_color, outline=outline_color, width=line_width)
else:
d.ellipse([cx - rx, cy2 - ry, cx + rx, cy2 + ry], fill=None, outline=outline_color, width=line_width)
d.line([cx - rx, cy1, cx - rx, cy2], fill=outline_color, width=line_width)
d.line([cx + rx, cy1, cx + rx, cy2], fill=outline_color, width=line_width)
d.ellipse([cx - rx, cy1 - ry, cx + rx, cy1 + ry], fill=None, outline=outline_color, width=line_width)
for i in range(len(t_chunks)-1, -1, -1):
t1, t2 = t_chunks[i]
y1 = paste_y + int(t1 * scale_y)
y2 = paste_y + int(t2 * scale_y)
true_mid_y = (y1 + y2) / 2
if y2 - y1 < ry * 2:
y1 = true_mid_y - ry
y2 = true_mid_y + ry
c = colors[i % len(colors)]
body_color = (c[0]//5, c[1]//5, c[2]//5, 200)
draw_cylinder(draw, bar_cx, y1, y2, rx, ry, fill_color=body_color, outline_color=c, line_width=2)
draw.line([bar_cx + rx + 5, true_mid_y, bar_cx + rx + 15, true_mid_y], fill=c, width=3)
draw.text((bar_cx + rx + 22, true_mid_y - 15), f"T{i+1}: F{t1}-{t2}", fill=c, font=f_info, stroke_width=3, stroke_fill=(0,0,0,255))
overlaps = []
for i in range(len(t_chunks)):
for j in range(i+1, len(t_chunks)):
a, b = t_chunks[i]
c, d = t_chunks[j]
o_start, o_end = max(a, c), min(b, d)
if o_start < o_end:
overlaps.append((o_start, o_end))
for o_start, o_end in overlaps:
y1 = paste_y + int(o_start * scale_y)
y2 = paste_y + int(o_end * scale_y)
true_mid_y = (y1 + y2) / 2
if y2 - y1 < ry * 2:
y1 = true_mid_y - ry
y2 = true_mid_y + ry
draw_cylinder(draw, bar_cx, y1, y2, rx, ry, fill_color=None, outline_color=(255, 255, 255, 255), line_width=4)
hl_color = (255, 255, 0, 255)
draw.line([bar_cx + rx + 4, true_mid_y, bar_cx + rx + 30, true_mid_y], fill=hl_color, width=3)
draw.text((bar_cx + rx + 35, true_mid_y - 15), "Overlap", fill=hl_color, font=f_info, stroke_width=2, stroke_fill=(0,0,0,255))
return (torch.from_numpy(np.array(full_canvas.convert("RGB")).astype(np.float32) / 255.0).unsqueeze(0),)