Add hybrid Euler 2S sampler node

This commit is contained in:
aimfordeb
2026-04-22 20:35:18 +03:00
parent 01039e5e03
commit a9a7bdb1f8
2 changed files with 201 additions and 16 deletions
+50
View File
@@ -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`.
+151 -16
View File
@@ -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",
}