From a9a7bdb1f8a8f1836416c637eb8dfc1fd8d391ac Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:35:18 +0300 Subject: [PATCH] Add hybrid Euler 2S sampler node --- docs/FunPackHybridEuler2SSampler.md | 50 +++++++++ funpack.py | 167 +++++++++++++++++++++++++--- 2 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 docs/FunPackHybridEuler2SSampler.md diff --git a/docs/FunPackHybridEuler2SSampler.md b/docs/FunPackHybridEuler2SSampler.md new file mode 100644 index 00000000..546c2050 --- /dev/null +++ b/docs/FunPackHybridEuler2SSampler.md @@ -0,0 +1,50 @@ +# FunPack Hybrid Euler 2S Sampler + +This node outputs a ComfyUI `SAMPLER` object that can be connected directly into `CustomSamplerAdvanced` / `SamplerCustomAdvanced`. + +## Purpose + +It is designed as a quality/speed compromise between: + +- `euler_ancestral`: fast, reliable, one model evaluation per step +- full 2S-style samplers: higher quality, but more expensive because they need an extra evaluation every step + +This sampler keeps classic Euler ancestral for most of the schedule, then applies a DPM-Solver++(2S)-style correction only on the late denoise steps where fine detail and cleanup usually matter most. + +## Parameters + +**eta**: Ancestral stochasticity. `1.0` keeps normal ancestral behavior. + +**s_noise**: Noise multiplier for ancestral noise injection. + +**high_quality_pct**: Fraction of the *late* denoise steps that receive the extra 2S correction. +Example: `0.35` means only the last 35% of steps pay the extra compute cost. + +**correction_blend**: Blend between pure Euler ancestral and the late-step 2S correction. + +- `0.0` = behaves like Euler ancestral +- `1.0` = full late-step correction + +## Recommended starting values + +- `eta = 1.0` +- `s_noise = 1.0` +- `high_quality_pct = 0.30` to `0.40` +- `correction_blend = 1.0` + +For a safer first test, try: + +- `high_quality_pct = 0.25` +- `correction_blend = 0.7` + +## Expected behavior + +Compared to plain `euler_ancestral`, this sampler should usually: + +- preserve more late-stage detail +- clean up texture and edge quality better +- cost noticeably less than a full 2S sampler + +## Limitation + +This node improves the **sampler-side quality/speed tradeoff**. It does **not** reduce the underlying cost of CFG++ guidance itself. If the goal becomes specifically “CFG++ quality at lower guidance cost”, that likely requires a custom `GUIDER`, not just a custom `SAMPLER`. diff --git a/funpack.py b/funpack.py index 94bce2ad..4ba9cf84 100644 --- a/funpack.py +++ b/funpack.py @@ -4,12 +4,14 @@ from transformers import LlamaConfig, LlamaForCausalLM, LlavaForConditionalGener from huggingface_hub import hf_hub_download, snapshot_download from safetensors.torch import load_file, save_file, safe_open import glob -import comfy.model_management as mm -import comfy.sd as sd -import folder_paths -import nodes -import tempfile -import gc # Import garbage collector +import comfy.model_management as mm +import comfy.model_sampling +import comfy.samplers +import comfy.sd as sd +import folder_paths +import nodes +import tempfile +import gc # Import garbage collector import torch.nn as nn import torch.nn.functional as F from typing import List, Tuple, Optional @@ -17,10 +19,12 @@ import numpy as np from PIL import Image, ImageDraw, ImageFont import folder_paths from comfy.utils import ProgressBar -import comfy.clip_vision -import math -import json -from comfy.utils import ProgressBar +import comfy.clip_vision +import comfy.k_diffusion.sampling as k_diffusion_sampling +import comfy.utils +import math +import json +from comfy.utils import ProgressBar import random import re import base64 @@ -1906,11 +1910,140 @@ class FunPackGemmaEmbeddingRefiner: modified_positive = [(new_positive, positive_meta)] return (modified_positive, status, feedback_question_output, training_info, loss_graph, refined_sigmas) + +@torch.no_grad() +def sample_funpack_hybrid_euler_2s(model, x, sigmas, extra_args=None, callback=None, + disable=None, eta=1.0, s_noise=1.0, + high_quality_pct=0.35, correction_blend=1.0): + """ + Hybrid sampler: + - Early schedule: classic Euler ancestral (fast). + - Late schedule: DPM-Solver++(2S)-style correction (higher quality) blended onto + the Euler ancestral proposal only where late-step detail matters most. + + This keeps the cost close to Euler ancestral while paying the extra model + evaluation only on the tail of the schedule. + """ + if isinstance(model.inner_model.inner_model.model_sampling, comfy.model_sampling.CONST): + return k_diffusion_sampling.sample_euler_ancestral( + model, x, sigmas, extra_args=extra_args, callback=callback, + disable=disable, eta=eta, s_noise=s_noise + ) + + extra_args = {} if extra_args is None else extra_args + seed = extra_args.get("seed", None) + noise_sampler = k_diffusion_sampling.default_noise_sampler(x, seed=seed) + + total_steps = max(0, len(sigmas) - 1) + if total_steps <= 0: + return x + + high_quality_pct = max(0.0, min(1.0, float(high_quality_pct))) + correction_blend = max(0.0, min(1.0, float(correction_blend))) + late_steps = max(1, int(math.ceil(total_steps * high_quality_pct))) if high_quality_pct > 0.0 else 0 + late_start = max(0, total_steps - late_steps) + + sigma_fn = lambda t: t.neg().exp() + t_fn = lambda sigma: sigma.log().neg() + s_in = x.new_ones([x.shape[0]]) + + for i in comfy.utils.model_trange(total_steps, disable=disable): + sigma = sigmas[i] + sigma_next = sigmas[i + 1] + denoised = model(x, sigma * s_in, **extra_args) + + if callback is not None: + callback({"x": x, "i": i, "sigma": sigma, "sigma_hat": sigma, "denoised": denoised}) + + sigma_down, sigma_up = k_diffusion_sampling.get_ancestral_step(sigma, sigma_next, eta=eta) + + if sigma_down == 0: + x = denoised + else: + d = k_diffusion_sampling.to_d(x, sigma, denoised) + dt = sigma_down - sigma + x_euler = x + d * dt + + if i >= late_start and correction_blend > 0.0: + t = t_fn(sigma) + t_next = t_fn(sigma_down) + h = t_next - t + r = 0.5 + s = t + r * h + + x_mid = (sigma_fn(s) / sigma_fn(t)) * x - torch.expm1(-h * r) * denoised + denoised_mid = model(x_mid, sigma_fn(s) * s_in, **extra_args) + x_2s = (sigma_fn(t_next) / sigma_fn(t)) * x - torch.expm1(-h) * denoised_mid + + x = x_euler.lerp(x_2s, correction_blend) + else: + x = x_euler + + if sigma_next > 0 and eta > 0 and s_noise > 0: + x = x + noise_sampler(sigma, sigma_next) * s_noise * sigma_up + + return x + + +class FunPackHybridEuler2SSampler: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "eta": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "tooltip": "Ancestral stochasticity. Keep at 1.0 for classic ancestral behaviour." + }), + "s_noise": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 10.0, + "step": 0.01, + "tooltip": "Noise scale for ancestral noise injection." + }), + "high_quality_pct": ("FLOAT", { + "default": 0.35, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "tooltip": "Fraction of late denoise steps that receive the slower 2S quality correction." + }), + "correction_blend": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "tooltip": "Blend between Euler ancestral (0.0) and late-step 2S correction (1.0)." + }), + } + } + + RETURN_TYPES = ("SAMPLER",) + RETURN_NAMES = ("sampler",) + FUNCTION = "get_sampler" + CATEGORY = "FunPack/Sampling" + DESCRIPTION = "Hybrid sampler: Euler ancestral for most steps, DPM-Solver++(2S)-style correction only on late steps for a better quality/speed tradeoff." + + def get_sampler(self, eta, s_noise, high_quality_pct, correction_blend): + sampler = comfy.samplers.KSAMPLER( + sample_funpack_hybrid_euler_2s, + extra_options={ + "eta": eta, + "s_noise": s_noise, + "high_quality_pct": high_quality_pct, + "correction_blend": correction_blend, + } + ) + return (sampler,) + # Constants from StoryMem IMAGE_FACTOR = 28 VIDEO_MIN_PIXELS = 48 * IMAGE_FACTOR * IMAGE_FACTOR # 37,632 -MIN_FRAME_SIMILARITY = 0.9 -MAX_KEYFRAME_NUM = 3 +MIN_FRAME_SIMILARITY = 0.9 +MAX_KEYFRAME_NUM = 3 ADAPTIVE_ALPHA = 0.01 HPSV3_QUALITY_THRESHOLD = 3.0 @@ -2998,11 +3131,12 @@ NODE_CLASS_MAPPINGS = { "FunPackContinueVideo": FunPackContinueVideo, "FunPackLorebookEnhancer": FunPackLorebookEnhancer, "FunPackGemmaEmbeddingRefiner": FunPackGemmaEmbeddingRefiner, + "FunPackHybridEuler2SSampler": FunPackHybridEuler2SSampler, } - -NODE_DISPLAY_NAME_MAPPINGS = { - "FunPackPromptCombiner": "FunPack Prompt Combiner", - "FunPackStoryMemKeyframeExtractor": "FunPack StoryMem Keyframe Extractor", + +NODE_DISPLAY_NAME_MAPPINGS = { + "FunPackPromptCombiner": "FunPack Prompt Combiner", + "FunPackStoryMemKeyframeExtractor": "FunPack StoryMem Keyframe Extractor", "FunPackStoryMemLastFrameExtractor": "FunPack StoryMem Last Frame Extractor", "FunPackPromptEnhancer": "FunPack Prompt Enhancer (Standalone)", "FunPackStoryWriter": "FunPack Story Writer", @@ -3010,4 +3144,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FunPackContinueVideo": "FunPack Continue Video", "FunPackLorebookEnhancer": "FunPack Lorebook Enhancer", "FunPackGemmaEmbeddingRefiner": "FunPack Gemma Embedding Refiner (Self-Refinement)", + "FunPackHybridEuler2SSampler": "FunPack Hybrid Euler 2S Sampler", }