12 · 采样器、调度器与 CFG
“KSampler”在 UI 里是一个节点,在源码里却是四层组合:节点适配、噪声准备、sigma scheduler、sampler algorithm,再由 CFGGuider 管条件和模型生命周期。把它们拆开,才能解释“同一个 sampler 换 scheduler 为什么画面会变”。
先分清四个概念
| 概念 | 回答的问题 | 例子 |
|---|---|---|
| noise | 从哪个随机初态开始 | seed 产生的高斯噪声 |
| scheduler | 每一步处于哪个噪声水平 | normal、karras、exponential |
| sampler | 怎样从当前状态推进到下一 sigma | euler、dpmpp、heun |
| guider | 条件/负条件如何组织并引导模型 | CFGGuider |
steps 决定离散点数量,但 scheduler 决定这些点在哪里;sampler 决定相邻点之间的数值更新公式。
完整调用链
KSampler node → common_ksampler → comfy.sample.prepare_noise → latent_preview.prepare_callback → comfy.sample.sample → comfy.samplers.KSampler.sample → CFGGuider.sample → sampler.sample → sampling_function → calc_cond_batch → BaseModel.apply_model
节点适配层:common_ksampler
nodes.pyL1564-1587latent、noise、mask、callback 与 sample 调用。
它做六件事:
- 从 latent dict 取
samples; - 通过模型 latent format 修正通道;
- 按 seed 构造 noise,或在 disable noise 时全零;
- 取可选
noise_mask; - 创建带预览的 callback;
- 调
comfy.sample.sample(),把结果写回 latent dict 的浅拷贝。
为什么返回 latent dict 而不是裸 tensor
除了 samples,LATENT 还可能携带 noise_mask、batch index、latent metadata 等。浅拷贝并只替换 samples 保留其余协议字段。
确定性噪声
prepare_noise() 在 CPU 上创建 torch.Generator 并以 seed 初始化,再按 latent shape 生成噪声;若有 noise_inds,可只为唯一 index 生成后重排。
CPU 生成有助于跨 device 路径保持更一致的初态。最终算法仍可能受不同 backend kernel、dtype 与非确定算子影响,seed 相同不代表所有硬件逐位一致。
disable_noise
使用全零 tensor,不表示没有 denoise 过程;它只改变起始噪声项。img2img / latent 输入仍按 sigma 轨迹演化。
comfy.sample.sample():薄但重要的边界
它构造 comfy.samplers.KSampler,把 noise、conditioning、latent、mask、callback 等交给 sampler,完成后把结果移到 intermediate device,并恢复期望 dtype。
窄边界让其他节点可以复用采样骨架,也让设备返回策略不散落在数值算法里。
KSampler:把名字解析为可执行组合
构造器保存 model、steps、device、sampler name、scheduler、denoise 与 model options,并计算 sigma 序列。
comfy/samplers.pyL1399-1463KSampler 初始化、sigma 与 sample 参数裁剪。sigma 序列
calculate_sigmas(model_sampling, scheduler_name, steps) 根据模型自己的 sampling 参数和 scheduler 名产生一维 tensor。
通常 sigma 从高噪声走向低/零噪声。不同 scheduler 对高低噪声区域分配步数不同;即使 sampler 和 steps 相同,轨迹也不同。
denoise 小于 1
代码用更长的基础步数构造 sigma,再截取尾部,使 img2img 从较低噪声强度开始。start_step、last_step 继续裁剪,可用于分阶段采样或高级控制。
force_full_denoise
若结束点本不为零,该选项强制最后 sigma 为零,让输出走完去噪。它会改变分段采样的衔接语义,不应盲目开启。
sampler_object
采样名字映射到包装器:部分调用 k-diffusion 函数,部分使用 UniPC 等实现,部分带额外选项。统一对象暴露类似:
sample(model_wrap, sigmas, extra_args, callback, noise,
latent_image, denoise_mask, disable_pbar)算法实现不需要知道 PromptQueue、WebSocket 或 ModelPatcher cache;这些由外层准备。
CFGGuider:真正的编排中心
comfy/samplers.pyL1188-1346条件准备、模型加载、采样包装与清理。CFGGuider.sample() / inner_sample() 负责:
- 预处理 positive / negative conditioning;
- 收集 conditioning 依赖的额外模型与内存;
- 调
load_models_gpu(); - 准备 mask、latent shapes、model options 和 hooks;
- 把模型包装为 sampler 期望的 callable;
- 调具体 sampler;
- 在 finally/cleanup 路径释放 hook 与临时状态;
- 解包 nested latent 并返回。
Sampler 只迭代数值步骤,Guider 管“每次模型评估到底带什么条件”。
CFG 的核心
普通形式可写为:
pred = uncond + cfg × (cond - uncond)源码中的 sampling_function() 先通过 calc_cond_batch() 得到 cond/uncond 预测,再调用 cfg function;model options 可替换最终 CFG 逻辑。
cfg = 1 的优化
当 scale 为 1 且没有其他选项强制 unconditional,结果等于 cond,运行时可以跳过 uncond forward。一次模型评估比两次便宜很多。
negative 不是简单“反向 prompt”
negative conditioning 产生 unconditional/negative 分支表示,CFG 使用两者差值引导。conditioning 还可携带 area、mask、strength、control、gligen、hooks、timestep 范围等元数据。
calc_cond_batch():最密集的条件逻辑
comfy/samplers.pyL208-608区域、mask、control、hook 与内存感知条件批处理。
对每个 conditioning 条目,它要:
- 判断当前 timestep 是否有效;
- 将 area 百分比/坐标转为 latent 切片;
- 裁剪输入到指定区域;
- 处理 mask 与 feathering;
- 组合 control / patch / transformer options;
- 判断多个 cond 能否拼成一个 batch;
- 根据空闲内存决定一次合并多少;
- 调 model.apply_model;
- 把区域输出按 strength 和 mask 累加回全图;
- 用计数/权重归一化重叠区域。
为什么要自己拼 batch
cond 与 uncond、多个区域若 shape 和控制选项兼容,可以一次 forward,减少模型调用;若显存不足则拆分。批处理不是固定 batch size,而是运行时基于内存与兼容性的装箱问题。
模型准备
在第一步 sampler 之前,Guider 收集主 MODEL 和 conditioning 引用的额外模型(如 ControlNet),估算额外内存并统一 load_models_gpu()。这确保显存管理看到完整集合,而不是主模型先独占空间后才发现还有 ControlNet。
sampling prepare callbacks 还允许 Patcher hooks 设置当前 transformer options;结束时 cleanup 恢复。
inpaint 与 mask
采样包装器可在每一步把未遮罩区域与原 latent 混合,并将噪声限制在 mask 区域。mask 会被 reshape、插值和广播到 latent batch/channel。
这不是 SaveImage 后的像素合成,而是在扩散轨迹中约束 latent。mask shape 与 latent batch 不匹配常在这一层暴露。
callback 与实时预览
节点层用 latent_preview.prepare_callback(model, steps, x0_output) 创建回调。sampler 每步提供 step、预测 x0、当前 x 与 total:
sampler step
→ callback
→ latent previewer decodes lightweight preview
→ ProgressBar.update_absolute
→ global progress hook
→ PromptServer WebSocket预览解码不是最终 VAE Decode,质量与颜色可能近似;它的目标是低延迟反馈。
中断
列表调用前与采样循环关键位置会检查 nodes.before_node_execution() / interrupt flag。取消是协作式的:Python 控制重新获得执行权时抛 InterruptProcessingException,执行器把它作为中断而非普通节点 bug 处理。
长时间单个 GPU kernel 无法在任意微秒安全终止。想改善中断响应,应缩短不可让出控制的工作块。
一步采样的数据形状
概念示例(具体通道取决于模型 latent format):
x [batch, latent_channels, h, w]
sigma [batch] or broadcastable scalar
conditioning token/context tensors + metadata
model output same spatial latent shape as xKSampler 节点的 IMAGE 分辨率通常已在 EmptyLatentImage 中除以 VAE scale 变成 latent 尺寸。不能直接拿像素 H×W 推断 sampler 内存。
一次采样为何可能调用模型多于 steps
- CFG 常需 cond + uncond;能否合批取决于内存和兼容性;
- 某些 sampler 每一步有多个 model evaluations;
- 区域 conditioning 可能拆成多个 batch;
- ControlNet / hooks 增加附加 forward;
- preview 不一定调用完整主模型,但会有解码开销。
所以 steps=20 不是“模型 forward 恰好 20 次”的保证。
参数变化落在哪一层
| UI 参数 | 主要影响层 |
|---|---|
| seed | noise generator |
| steps | sigma 数量 / sampler 迭代 |
| cfg | guider 合成,可能决定是否算 uncond |
| sampler_name | 数值积分/更新算法 |
| scheduler | sigma 分布 |
| denoise | sigma 轨迹截取 |
| positive / negative | conditioning 与 cond batch |
| latent batch size | noise shape、模型 batch、显存 |
常见调试路径
同 seed 结果不同
检查 model/LoRA、scheduler、sampler、steps、dtype/backend、conditioning 顺序、随机自定义节点与缓存 fingerprint,不要只看 seed。
cfg 提高后更慢
cfg=1 可能跳过 uncond;提高后恢复双分支。cond/uncond 是否能合批又受显存影响。
steps 不多却 OOM
峰值主要由模型、分辨率、batch、attention 和 cond batch 决定,steps 更影响时间,不线性决定峰值显存。
预览正常、最终 Decode OOM
轻量 latent preview 与完整 VAE Decode 是不同路径。使用 tiled VAE 或降低输出 batch,而不是先换 sampler。
可迁移的设计方法
- 将轨迹(scheduler)、更新法(sampler)和条件引导(guider)正交组合。
- 在进入数值内核前统一加载主模型与附属模型,做总预算。
- 条件批处理同时考虑 shape/option 兼容与实时内存,不写死 batch。
- 进度回调只报告领域数据,由 composition root 接到传输层。
- 确定性是整条链的属性,seed 只控制初始随机源。