271 lines
9.2 KiB
Python
271 lines
9.2 KiB
Python
"""
|
||
V-Sampler:Rust (`vates_sampler_core`) 与 Comfy `sample_custom` 组合出的手写采样入口。
|
||
|
||
- σ:Rust 纯实现 **karras / exponential / polyexponential**(与 Comfy `k_diffusion/sampling.py` + `BasicScheduler` 截取一致);其它调度降级为 `comfy.samplers.calculate_sigmas`。
|
||
- **噪声**:Rust **PCG** 生成的 **对齐 B×C×H×W f32**,经 NumPy→`torch.from_numpy` 映射至设备;
|
||
- **显存哨兵**:每步预览回调链路中耦合 `memory_monitor_tick`(可选 NVML),并配合 `cuda.empty_cache` 做 batch 碎片化平滑。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import gc
|
||
import importlib.util
|
||
import logging
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import latent_preview
|
||
|
||
import comfy.model_management as model_management
|
||
|
||
import comfy.sample as comfy_sample
|
||
import comfy.samplers as comfy_samplers
|
||
import comfy.utils as comfy_utils
|
||
import numpy as np
|
||
import torch
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_SAMPLER_ROOT = Path(__file__).resolve().parent
|
||
_DCT_CORE = _SAMPLER_ROOT / "dct-core"
|
||
if _DCT_CORE.is_dir() and str(_DCT_CORE) not in sys.path:
|
||
sys.path.insert(0, str(_DCT_CORE))
|
||
|
||
_SCHED_RUST = frozenset({"karras", "exponential", "polyexponential"})
|
||
_MAX_BATCH_HINT = 9
|
||
|
||
|
||
def _pick_sampler_core(): # noqa: ANN401
|
||
"""优先加载 `dct-core` 对齐的 ``vates_sampler_core`` 扩展模块。"""
|
||
for cand in ("vates_sampler_core.pyd", "vates_sampler_core.so"):
|
||
p = _DCT_CORE / cand
|
||
if not p.is_file():
|
||
continue
|
||
spec = importlib.util.spec_from_file_location("vates_sampler_core_ext_shim", p)
|
||
if spec is None or spec.loader is None:
|
||
continue
|
||
mod = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(mod)
|
||
return mod
|
||
try:
|
||
import vates_sampler_core as vsc # type: ignore
|
||
except ImportError:
|
||
raise ImportError(
|
||
"未找到 `vates_sampler_core`。请在 `ComfyUI-Vates-Sampler/dct-core/` 目录执行:`python install.py`。"
|
||
) from None
|
||
return vsc
|
||
|
||
|
||
def _sampler_core_or_none(): # noqa: ANN401
|
||
try:
|
||
return _pick_sampler_core()
|
||
except ImportError as e:
|
||
logger.warning("%s", e)
|
||
return None
|
||
|
||
|
||
def _batch_hint(b: int) -> None:
|
||
if b > _MAX_BATCH_HINT:
|
||
logger.warning(
|
||
"V-Sampler:batch=%s 超过与批量 Loader(9 槽)软性对齐的上限提示;仍可运行,OOM 时请降 batch。",
|
||
b,
|
||
)
|
||
|
||
|
||
def _sigma_tensor_from_python(model, scheduler: str, steps: int, denoise: float) -> torch.Tensor:
|
||
"""与 `BasicScheduler.execute`:`total_steps = int(steps/denoise)`、`sigmas[-(steps+1):]` 对齐。"""
|
||
if denoise < 1.0:
|
||
if denoise <= 0.0:
|
||
raise ValueError("denoise 须为正;Comfy BasicScheduler 在 denoise≤0 时返回空 sigma,本节点同样拒绝该配置。")
|
||
total_steps = max(1, int(steps / denoise))
|
||
else:
|
||
total_steps = steps
|
||
|
||
ms = model.get_model_object("model_sampling")
|
||
sigmas = comfy_samplers.calculate_sigmas(ms, scheduler, total_steps).cpu().float()
|
||
if sigmas.shape[-1] > steps + 1:
|
||
sigmas = sigmas[-(steps + 1) :]
|
||
return sigmas
|
||
|
||
|
||
def _sigma_tensor_pref_rust(core, model, scheduler: str, steps: int, denoise: float) -> torch.Tensor:
|
||
ls = scheduler.lower().strip()
|
||
if ls not in _SCHED_RUST:
|
||
return _sigma_tensor_from_python(model, scheduler, steps, denoise)
|
||
ms = model.get_model_object("model_sampling")
|
||
arr = core.build_sigma_schedule(
|
||
ls,
|
||
float(ms.sigma_min),
|
||
float(ms.sigma_max),
|
||
int(steps),
|
||
float(denoise),
|
||
7.0,
|
||
1.0,
|
||
)
|
||
flat = np.ascontiguousarray(np.asarray(arr), dtype=np.float32)
|
||
return torch.from_numpy(flat)
|
||
|
||
|
||
def _rust_latent_noise(
|
||
core,
|
||
shape_bchw: tuple[int, int, int, int],
|
||
seed: int,
|
||
*,
|
||
dtype: torch.dtype,
|
||
device: torch.device,
|
||
) -> torch.Tensor:
|
||
b, c, h, w = shape_bchw
|
||
arr = core.latent_gaussian_noise_f32(b, c, h, w, int(seed) & 0xFFFFFFFFFFFFFFFF, 0)
|
||
t = torch.from_numpy(np.asarray(arr))
|
||
blk = getattr(model_management, "device_supports_non_blocking", None)
|
||
nb = blk(device) if callable(blk) else False
|
||
return t.to(device=device, dtype=dtype, non_blocking=nb)
|
||
|
||
|
||
class VatesAdvancedSampler:
|
||
@classmethod
|
||
def INPUT_TYPES(cls) -> dict: # noqa: N802
|
||
return {
|
||
"required": {
|
||
"model": ("MODEL",),
|
||
"seed": (
|
||
"INT",
|
||
{"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
|
||
),
|
||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||
"cfg": (
|
||
"FLOAT",
|
||
{"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.01},
|
||
),
|
||
"sampler_name": (comfy_samplers.SAMPLER_NAMES, {}),
|
||
"scheduler": (comfy_samplers.SCHEDULER_NAMES, {}),
|
||
"positive": ("CONDITIONING",),
|
||
"negative": ("CONDITIONING",),
|
||
"latent_image": ("LATENT",),
|
||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("LATENT",)
|
||
RETURN_NAMES = ("latent",)
|
||
FUNCTION = "sample"
|
||
CATEGORY = "Vates/Sampling"
|
||
|
||
def sample(
|
||
self,
|
||
model,
|
||
seed: int,
|
||
steps: int,
|
||
cfg: float,
|
||
sampler_name: str,
|
||
scheduler: str,
|
||
positive,
|
||
negative,
|
||
latent_image: dict,
|
||
denoise: float,
|
||
) -> tuple[dict]:
|
||
core = _sampler_core_or_none()
|
||
|
||
latent = latent_image.copy()
|
||
latent_image_ts = latent["samples"]
|
||
if getattr(latent_image_ts, "is_nested", False):
|
||
raise RuntimeError("V-Sampler:暂不支持 NestedTensor latent — 请使用内置采样节点。")
|
||
|
||
latent_image_ts = comfy_sample.fix_empty_latent_channels(
|
||
model,
|
||
latent_image_ts,
|
||
latent.get("downscale_ratio_spacial"),
|
||
)
|
||
latent["samples"] = latent_image_ts
|
||
|
||
_batch_hint(latent_image_ts.shape[0])
|
||
|
||
try:
|
||
sampler = comfy_samplers.sampler_object(sampler_name)
|
||
except Exception as e: # noqa: BLE001
|
||
raise RuntimeError(f"无效的 sampler_name={sampler_name!r}") from e
|
||
|
||
if core is not None:
|
||
sigmas = _sigma_tensor_pref_rust(core, model, scheduler, steps, denoise)
|
||
else:
|
||
sigmas = _sigma_tensor_from_python(model, scheduler, steps, denoise)
|
||
|
||
if sigmas.numel() < 2:
|
||
raise RuntimeError("Sigma 序列过短;请增大 denoise/steps。")
|
||
|
||
sigmas_device = sigmas.to(model.load_device)
|
||
|
||
dtype = latent_image_ts.dtype
|
||
device = latent_image_ts.device
|
||
if core is None:
|
||
noise_t = comfy_sample.prepare_noise(latent_image_ts, int(seed))
|
||
else:
|
||
b, c, h, w = (
|
||
latent_image_ts.shape[0],
|
||
latent_image_ts.shape[1],
|
||
latent_image_ts.shape[2],
|
||
latent_image_ts.shape[3],
|
||
)
|
||
noise_t = _rust_latent_noise(core, (b, c, h, w), int(seed), dtype=dtype, device=device)
|
||
|
||
noise_mask = latent.get("noise_mask")
|
||
|
||
x0_payload: dict = {}
|
||
preview_cb = latent_preview.prepare_callback(
|
||
model,
|
||
sigmas_device.shape[-1] - 1,
|
||
x0_payload,
|
||
)
|
||
|
||
def vacuum_callback(step_idx: int, latent_denoised, x_lat, total_steps):
|
||
if core is not None:
|
||
try:
|
||
s_np = np.ascontiguousarray(sigmas.detach().cpu().numpy(), dtype=np.float64)
|
||
if 0 <= int(step_idx) < s_np.size - 1:
|
||
_ = core.sigma_pair_instruction(s_np, int(step_idx))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
try:
|
||
st = core.memory_monitor_tick(0.90)
|
||
if isinstance(st, dict) and st.get("vacuum_hot"):
|
||
gc.collect()
|
||
except Exception: # noqa: BLE001
|
||
gc.collect()
|
||
else:
|
||
gc.collect()
|
||
|
||
if torch.cuda.is_available():
|
||
try:
|
||
torch.cuda.empty_cache()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
if preview_cb is not None:
|
||
preview_cb(step_idx, latent_denoised, x_lat, total_steps)
|
||
|
||
disable_pbar = not comfy_utils.PROGRESS_BAR_ENABLED
|
||
sampled = comfy_sample.sample_custom(
|
||
model,
|
||
noise_t,
|
||
float(cfg),
|
||
sampler,
|
||
sigmas_device,
|
||
positive,
|
||
negative,
|
||
latent_image_ts,
|
||
noise_mask=noise_mask,
|
||
callback=vacuum_callback,
|
||
disable_pbar=disable_pbar,
|
||
seed=int(seed),
|
||
)
|
||
|
||
latent_out = latent.copy()
|
||
latent_out.pop("downscale_ratio_spacial", None)
|
||
latent_out["samples"] = sampled.to(
|
||
dtype=model_management.intermediate_dtype(),
|
||
device=model_management.intermediate_device(),
|
||
)
|
||
logger.debug("V-Sampler 完成 sampler=%s scheduler=%s steps=%s", sampler_name, scheduler, steps)
|
||
return (latent_out,)
|