Files
digital-garbageandClaude Sonnet 5.5 f5050a4574 Shorter in-app hints: what a setting does, where to start, what it needs
Long hints for 33 settings are cut to one or two sentences each, and the
Taste steering strength and blocks hints now give a starting value and
recommended blocks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 13:26:17 +02:00

147 lines
6.3 KiB
Python

"""PTD few-step adapters for MiniMax-H3: 4 to 8 hops instead of a long schedule, with no CFG.
A PTD file is not a LoRA: it holds one set of adapters per hop, and the sampler must visit exactly the
times it was trained at. Switching this on loads the file, puts the adapters on the model and runs PTD's
own sampler over the file's hop times, whatever sampler and steps are chosen. Ordinary LoRAs stack with it.
Loads the EPFL fl2va releases and the taobao-content-ai Ref2VA releases, as bf16 or as the smaller int8
files, on the full or the pruned base. Ported from ComfyUI-PTD-Compact (Nynxz) over ComfyUI-PTD (EPFL VITA
Lab) and ComfyUI-MiniMaxH3-PTD (taobao-content-ai), all Apache-2.0; see the README Thank you section.
Not yet run on a real H3 here.
"""
import os
from pathlib import Path
import folder_paths
from comfy.patcher_extension import WrappersMP
from ..._core import control, log, patching
from . import adapters
from .controller import CONTROLLER_KEY, Controller, attach
from .sampler import sample_ptd
ID = "ptd"
TITLE = "PTD few-step"
MOUNT = "generation.sampling"
STAGE = "load"
CATEGORY = "sampling"
STATUS = "experimental"
REQUIRES = ["audio_stream", "adaln_modalities"]
if "ptd" not in folder_paths.folder_names_and_paths:
folder_paths.add_model_folder_path("ptd", str(Path(folder_paths.models_dir) / "ptd"))
KERNELS = {"exact": False, "fast": True}
def _files():
try:
return [f for f in folder_paths.get_filename_list("ptd") if f.endswith(".safetensors")]
except Exception: # noqa: BLE001 -- no folder yet is the same as no files
return []
def _options():
return [{"value": f, "label": f} for f in _files()] or [{"value": "", "label": "No file in models/ptd"}]
_FOUND = _files()
SETTINGS = {
"enabled": {
"type": "bool", "default": False,
"label": "Few-step PTD",
"hint": "Renders in the adapter file's own 4 to 8 steps, without CFG. Use the base the file was trained on, with SLA, turbo LoRAs and other enhancements off. Cannot start from a picture or mask.",
},
"adapter": {
"type": "enum", "default": _FOUND[0] if _FOUND else "",
"options": _options(),
"label": "PTD adapter file",
"hint": "A PTD file from ComfyUI/models/ptd. The list is read again each time Settings opens.",
"when": {"enabled": True},
},
"kernels": {
"type": "enum", "default": "exact",
"options": [{"value": "exact", "label": "Exact"}, {"value": "fast", "label": "Fast"}],
"label": "Block maths",
"hint": "Exact: matches the training arithmetic. Fast: 4 to 8 percent quicker per step, only the rounding differs.",
"when": {"enabled": True},
},
}
# One parsed file is kept between runs, so changing a seed or a prompt does not read gigabytes again.
# It holds no model, so nothing here keeps a diffusion model alive.
_kept = {}
def _adapter(path):
stamp = (str(path), os.path.getmtime(path), os.path.getsize(path))
if _kept.get("stamp") != stamp:
_kept.clear()
_kept.update(stamp=stamp, adapter=adapters.parse(path, label=Path(path).name))
return _kept["adapter"]
def _swap(controller):
"""Run PTD's sampler over the adapter's own hop times, in place of the sampler that was chosen."""
import comfy.samplers
ptd = comfy.samplers.KSAMPLER(sample_ptd, extra_options={"controller": controller})
def run(executor, guider, sigmas, extra_args, callback, noise, latent_image, denoise_mask, disable_pbar):
if abs(float(getattr(guider, "cfg", 1.0)) - 1.0) > 1e-6:
log.once("ptd:cfg", log.ALERT, "FunUI PTD",
f"CFG is {guider.cfg:g}: PTD was trained without guidance, so set CFG to 1")
try:
import latent_preview
callback = latent_preview.prepare_callback(guider.model_patcher, controller.hops)
except Exception: # noqa: BLE001 -- only the progress bar and previews are lost
pass
# Step-aware modules (SLA, momentum, velocity bias) read the schedule from here: give them the real one.
# On the device and dtype the sampler was given: ComfyUI's own schedule sits on the GPU, and a model that
# compares it with its call's sigma (H3's head bank, SLA) fails on a CPU copy.
grid = controller.grid.to(device=sigmas.device, dtype=sigmas.dtype)
options = extra_args.get("model_options", {}).get("transformer_options")
if options is not None:
options["sample_sigmas"] = grid
log.once(f"ptd:swap:{controller.adapter.label}:{controller.hops}", log.INFO, "FunUI PTD",
f"sampling {controller.hops} hops from {controller.adapter.label}; the chosen sampler and steps "
f"({len(sigmas) - 1}) are not used")
return ptd.sample(guider, grid, extra_args, callback, noise, latent_image,
denoise_mask, disable_pbar)
return run
def install(patcher, values, key):
if not values["enabled"]:
_kept.clear()
return None
raw = getattr(patcher, "_patcher", patcher)
# PTD installs on the real patcher, not the guarded one. A guard would drop a failing hook and carry on
# with the plain sampler on a model whose layers expect PTD's hops, which renders noise; here it ends the run.
path = folder_paths.get_full_path("ptd", values["adapter"]) if values["adapter"] else None
if path is None:
raise control.Unavailable("no PTD adapter file is chosen: put one in ComfyUI/models/ptd, restart, "
"and pick it in Settings")
native = raw.get_model_object("diffusion_model")
adapter = _adapter(path)
adapters.verify(adapter, native)
if CONTROLLER_KEY in raw.model_options:
raise ValueError("This model already carries PTD adapters")
controller = Controller(native, adapter)
controller.fuse_blocks = KERNELS[values["kernels"]]
attach(raw, controller, key, patching.tag)
raw.add_wrapper_with_key(WrappersMP.SAMPLER_SAMPLE, key, patching.tag(_swap(controller), key))
size = adapter.size / 2**30
return (f"{adapter.label}, {adapter.hops} hops, {'int8' if adapter.int8 else 'float'} {size:.1f} GB, "
f"{values['kernels']} blocks")
def patcher_has_ptd(patcher):
from .controller import CONTROLLER_KEY
return CONTROLLER_KEY in patcher.model_options
PROVIDES = {"modifier": install, "setting_options": lambda: {"adapter": _options()}}