Files
iGavroche-rocm-ninodes/rocm_nodes/core/sampler.py
T
iGavroche b3b82e1cb8 perf: fix ROCm SamplerCustomAdvanced callback overhead
- Create previewer ONCE outside callback (was creating every 5 steps)
- Console timing at 10% milestones only instead of every step
- Prevent blocking sampling loop with excessive I/O
2026-06-13 11:05:27 -04:00

781 lines
34 KiB
Python

"""
Sampler nodes for ROCM Ninodes.
Contains all sampler-related node implementations:
- ROCMOptimizedKSampler: Basic ROCm-optimized sampler
- ROCMOptimizedKSamplerAdvanced: Advanced sampler with more control
- ROCMSamplerPerformanceMonitor: Performance monitoring and recommendations
"""
import time
import logging
from typing import Dict, Any, Tuple, Optional
import torch
import comfy.model_management as model_management
import comfy.utils
import comfy.sample
import comfy.samplers
import latent_preview
from comfy_api.latest import io
from ..utils.memory import (
gentle_memory_cleanup,
emergency_memory_cleanup,
get_gpu_memory_info,
)
from ..utils.debug import (
DEBUG_MODE,
log_debug,
capture_timing,
capture_memory_usage,
)
from ..utils.architecture import (
detect_architecture,
detect_model_sampling_type,
apply_rocm_backend_settings,
)
class ROCMOptimizedKSampler:
"""
Stock KSampler behavior under ROCm category for compatibility.
Auto-detects GPU architecture and model type for optimal settings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {"tooltip": "The model to sample from"}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"tooltip": "Random seed for generation"
}),
"steps": ("INT", {
"default": 20,
"min": 1,
"max": 10000,
"tooltip": "Number of sampling steps"
}),
"cfg": ("FLOAT", {
"default": 8.0,
"min": 0.0,
"max": 100.0,
"step": 0.1,
"round": 0.01,
"tooltip": "Classifier-free guidance scale"
}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {
"tooltip": "Sampling algorithm to use"
}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {
"tooltip": "Scheduler for noise timesteps"
}),
"positive": ("CONDITIONING", {"tooltip": "Positive conditioning"}),
"negative": ("CONDITIONING", {"tooltip": "Negative conditioning"}),
"latent_image": ("LATENT", {"tooltip": "Latent image to sample from"}),
"denoise": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Denoising strength"
})
},
"optional": {
"optimize_for_video": ("BOOLEAN", {"default": False, "tooltip": "Disable previews/progress for multi-frame latents"}),
"precision_mode": (["auto", "fp32", "bf16"], {"default": "auto", "tooltip": "ROCm precision hint (no-op on CUDA/CPU)"}),
"compatibility_mode": ("BOOLEAN", {"default": False, "tooltip": "Force pure stock behavior"})
}
}
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("LATENT",)
FUNCTION = "sample"
CATEGORY = "ROCm Ninodes/Sampling"
DESCRIPTION = "KSampler with auto-detected architecture and model optimizations"
def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
latent_image, denoise=1.0, optimize_for_video=False, precision_mode="auto", compatibility_mode=False):
# ── Auto-detect architecture and model type ──────────────────────────
arch_info = detect_architecture()
model_info = detect_model_sampling_type(model)
is_rocm = bool(getattr(torch.version, 'hip', None))
if not compatibility_mode:
apply_rocm_backend_settings(arch_info)
# Aggressive cleanup for high-memory models (Ideogram4 etc.)
if model_info["has_high_memory"]:
print(f"💾 High-memory model detected (factor: {model_info['memory_usage_factor']}) — cleaning up")
emergency_memory_cleanup()
# Log model type
if model_info["model_type"] == "flow":
print(f"🌊 Flow-matching model detected ({model_info['latent_channels']}ch latent)")
elif model_info["is_pixel_space"]:
print(f"📷 Pixel-space model detected — no VAE decode needed downstream")
elif model_info["has_high_memory"]:
print(f"🔋 Large model detected — memory management enabled")
use_bf16 = False
if not compatibility_mode and precision_mode == "bf16" and is_rocm and torch.cuda.is_available():
try:
is_supported = getattr(torch.cuda, 'is_bf16_supported', lambda: False)()
use_bf16 = bool(is_supported)
except Exception:
use_bf16 = False
latent = latent_image
latent_image_tensor = latent["samples"]
latent_image_tensor = comfy.sample.fix_empty_latent_channels(model, latent_image_tensor)
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image_tensor, seed, batch_inds)
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
# Optional ROCm-aware knobs (default to stock behavior)
use_video_optim = False
is_video_workflow = False
if not compatibility_mode and optimize_for_video and latent_image_tensor.ndim == 5 and latent_image_tensor.shape[2] > 1:
use_video_optim = True
is_video_workflow = True
disable_pbar = use_video_optim
pbar = comfy.utils.ProgressBar(steps)
last_update_time = [time.time()]
start_time = time.time()
def enhanced_callback(step, x0, x, total_steps):
current_time = time.time()
if current_time - last_update_time[0] >= 0.3 or step == total_steps - 1:
preview_bytes = None
if not is_video_workflow and step % 5 == 0:
try:
previewer = latent_preview.get_previewer(model.load_device, model.model.latent_format)
if previewer:
preview_bytes = previewer.decode_latent_to_preview_image("JPEG", x0)
except:
preview_bytes = None
try:
pbar.update_absolute(step + 1, total_steps, preview=preview_bytes)
except:
pass
last_update_time[0] = current_time
progress_pct = ((step + 1) / total_steps) * 100
elapsed = current_time - start_time
if step > 0:
est_total = elapsed / ((step + 1) / total_steps)
est_remaining = est_total - elapsed
avg_time_per_step = elapsed / (step + 1)
workflow_type = "Video" if is_video_workflow else "Image"
print(f"📊 {workflow_type} KSampler: Step {step + 1}/{total_steps} ({progress_pct:.1f}%) | "
f"Elapsed: {elapsed:.1f}s | Remaining: ~{est_remaining:.1f}s | "
f"Avg: {avg_time_per_step:.2f}s/step", flush=True)
else:
workflow_type = "Video" if is_video_workflow else "Image"
print(f"📊 {workflow_type} KSampler: Step {step + 1}/{total_steps} ({progress_pct:.1f}%) | Starting...", flush=True)
callback = enhanced_callback
print(f"🚀 Starting sampling: {steps} steps, CFG {cfg}, {sampler_name} ({scheduler})", flush=True)
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image_tensor,
denoise=denoise, noise_mask=noise_mask, callback=callback, disable_pbar=False, seed=seed)
print(f"✅ Sampling completed", flush=True)
# Gentle cleanup after sampling (if model was large)
if model_info["has_high_memory"]:
gentle_memory_cleanup()
out = latent.copy()
out["samples"] = samples
return (out,)
class ROCMOptimizedKSamplerAdvanced:
"""
Stock KSampler (Advanced) - exact copy from ComfyUI.
This is a 1:1 copy to ensure identical behavior.
No ROCm-specific modifications.
"""
@classmethod
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"add_noise": (["enable", "disable"], ),
"noise_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.KSampler.SAMPLERS, ),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"return_with_leftover_noise": (["disable", "enable"], ),
},
"optional": {
"optimize_for_video": ("BOOLEAN", {"default": False, "tooltip": "Disable previews/progress for multi-frame latents"}),
"precision_mode": (["auto", "fp32", "bf16"], {"default": "auto", "tooltip": "ROCm precision hint (no-op on CUDA/CPU)"}),
"compatibility_mode": ("BOOLEAN", {"default": False, "tooltip": "Force pure stock behavior"})
}
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "sample"
CATEGORY = "ROCm Ninodes/Sampling"
DESCRIPTION = "Advanced KSampler with step control (stock ComfyUI implementation)"
def sample(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0, optimize_for_video=False, precision_mode="auto", compatibility_mode=False):
arch_info = detect_architecture()
model_info = detect_model_sampling_type(model)
is_rocm = bool(getattr(torch.version, 'hip', None))
if not compatibility_mode:
apply_rocm_backend_settings(arch_info)
if model_info["has_high_memory"]:
print(f"💾 High-memory model detected (factor: {model_info['memory_usage_factor']}) — cleaning up")
emergency_memory_cleanup()
if model_info["model_type"] == "flow":
print(f"🌊 Flow-matching model detected ({model_info['latent_channels']}ch latent)")
elif model_info["is_pixel_space"]:
print(f"📷 Pixel-space model detected — no VAE decode needed downstream")
use_bf16 = False
if not compatibility_mode and precision_mode == "bf16" and is_rocm and torch.cuda.is_available():
try:
is_supported = getattr(torch.cuda, 'is_bf16_supported', lambda: False)()
use_bf16 = bool(is_supported)
except Exception:
use_bf16 = False
force_full_denoise = True
if return_with_leftover_noise == "enable":
force_full_denoise = False
disable_noise = False
if add_noise == "disable":
disable_noise = True
latent = latent_image
latent_image_tensor = latent["samples"]
latent_image_tensor = comfy.sample.fix_empty_latent_channels(model, latent_image_tensor)
if disable_noise:
noise = torch.zeros(latent_image_tensor.size(), dtype=latent_image_tensor.dtype, layout=latent_image_tensor.layout, device="cpu")
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image_tensor, noise_seed, batch_inds)
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
use_video_optim = False
is_video_workflow = False
if not compatibility_mode and optimize_for_video and latent_image_tensor.ndim == 5 and latent_image_tensor.shape[2] > 1:
use_video_optim = True
is_video_workflow = True
disable_pbar = use_video_optim
pbar = comfy.utils.ProgressBar(steps)
last_update_time = [time.time()]
start_time = time.time()
def enhanced_callback(step, x0, x, total_steps):
current_time = time.time()
if current_time - last_update_time[0] >= 0.3 or step == total_steps - 1:
preview_bytes = None
if not is_video_workflow and step % 5 == 0:
try:
previewer = latent_preview.get_previewer(model.load_device, model.model.latent_format)
if previewer:
preview_bytes = previewer.decode_latent_to_preview_image("JPEG", x0)
except:
preview_bytes = None
try:
pbar.update_absolute(step + 1, total_steps, preview=preview_bytes)
except:
pass
last_update_time[0] = current_time
progress_pct = ((step + 1) / total_steps) * 100
elapsed = current_time - start_time
if step > 0:
est_total = elapsed / ((step + 1) / total_steps)
est_remaining = est_total - elapsed
avg_time_per_step = elapsed / (step + 1)
workflow_type = "Video" if is_video_workflow else "Image"
print(f"📊 Advanced {workflow_type} KSampler: Step {step + 1}/{total_steps} ({progress_pct:.1f}%) | "
f"Elapsed: {elapsed:.1f}s | Remaining: ~{est_remaining:.1f}s | "
f"Avg: {avg_time_per_step:.2f}s/step", flush=True)
else:
workflow_type = "Video" if is_video_workflow else "Image"
print(f"📊 Advanced {workflow_type} KSampler: Step {step + 1}/{total_steps} ({progress_pct:.1f}%) | Starting...", flush=True)
callback = enhanced_callback
print(f"🚀 Starting advanced sampling: {steps} steps (range: {start_at_step}-{end_at_step}), CFG {cfg}, {sampler_name} ({scheduler})", flush=True)
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image_tensor,
denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step,
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=False, seed=noise_seed)
print(f"✅ Advanced sampling completed", flush=True)
if model_info["has_high_memory"]:
gentle_memory_cleanup()
out = latent.copy()
out["samples"] = samples
return (out,)
class ROCMSamplerPerformanceMonitor:
"""Monitor sampler performance and provide optimization suggestions"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {"tooltip": "Model to analyze"}),
"test_steps": ("INT", {
"default": 20,
"min": 1,
"max": 100,
"tooltip": "Number of test steps for benchmarking"
})
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("DEVICE_INFO", "PERFORMANCE_TIPS", "OPTIMAL_SETTINGS")
FUNCTION = "analyze"
CATEGORY = "ROCm Ninodes/Sampling"
DESCRIPTION = "Analyze sampler performance and provide optimization recommendations"
def analyze(self, model, test_steps=20):
arch_info = detect_architecture()
model_info = detect_model_sampling_type(model)
device_info = f"Architecture: {arch_info['family']}\n"
device_info += f"APU mode: {arch_info['is_apu']}\n"
try:
device = model_management.get_torch_device()
model_dtype = model.model_dtype()
device_info += f"Model device: {device}\n"
device_info += f"Model dtype: {model_dtype}\n"
except:
device_info += "Model info unavailable\n"
if torch.cuda.is_available():
try:
device_name = torch.cuda.get_device_name(0)
device_info += f"GPU: {device_name}\n"
except:
device_info += "GPU: AMD (ROCm)\n"
device_info += f"Sampling type: {model_info['model_type']}\n"
device_info += f"Latent channels: {model_info['latent_channels']}\n"
device_info += f"Memory factor: {model_info['memory_usage_factor']}x\n"
tips = []
settings = []
is_amd = arch_info["family"] != "cpu"
# Model-type tips
if model_info["is_pixel_space"]:
tips.append("• Pixel-space model — no VAE decode needed, lower overall memory")
settings.append("No VAE decode configuration needed")
elif model_info["model_type"] == "flow":
tips.append("• Flow-matching model — use euler / dpmpp_sde / dpmpp_2m samplers")
tips.append("• sgm_uniform scheduler recommended for flow matching")
if model_info["latent_channels"] >= 128:
tips.append(f"• Large latent format ({model_info['latent_channels']}ch) — ensure adequate memory")
elif model_info["model_type"] == "eps":
tips.append("• Standard noise-prediction model")
elif model_info["model_type"] == "v_prediction":
tips.append("• V-prediction model")
else:
tips.append("• Unknown model type — standard settings applied")
# Architecture tips
if is_amd:
pref = arch_info.get("preferred_precision", "fp16")
tips.append(f"• {pref.upper()} recommended for {arch_info['family']}")
tips.append("• Enable ROCm optimizations for best results")
if arch_info["is_apu"]:
tips.append("• APU mode: disable smart-memory for unified memory")
settings.append(f"Recommended precision: {pref}")
settings.append("Recommended samplers: euler, dpmpp_2m, dpmpp_sde")
settings.append(f"Recommended schedulers: sgm_uniform, normal")
else:
tips.append("• Use fp16 or bf16 for better performance")
settings.append("Recommended precision: auto")
settings.append("Recommended samplers: dpmpp_2m, dpmpp_sde")
return (
device_info,
"\n".join(tips),
"\n".join(settings)
)
class ROCMSamplerCustomAdvanced(io.ComfyNode):
"""
Drop-in replacement for SamplerCustomAdvanced with ROCm optimizations.
Preserves the exact same interface (noise, guider, sampler, sigmas, latent_image)
while adding architecture-aware ROCm backend tuning, memory management for
high-memory models (LTX Video, etc.), and detailed per-step progress tracking.
Key enhancements over stock SamplerCustomAdvanced:
- Auto-detects AMD GPU architecture and applies optimal backend settings
- Emergency memory defrag for high-memory models (128ch latents)
- Optional BF16 precision hint for supported AMD GPUs
- Enhanced callback with ETA, per-step timing, and video-workflow optimization
- Gentle post-sample memory cleanup
"""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ROCMSamplerCustomAdvanced",
display_name="ROCM SamplerCustomAdvanced",
category="ROCm Ninodes/Sampling",
description="Drop-in replacement for SamplerCustomAdvanced with ROCm memory management, backend tuning, and detailed progress tracking. Particularly beneficial for high-memory models like LTX Video (128ch latent).",
inputs=[
io.Noise.Input("noise"),
io.Guider.Input("guider"),
io.Sampler.Input("sampler"),
io.Sigmas.Input("sigmas"),
io.Latent.Input("latent_image"),
io.Boolean.Input("optimize_for_video", optional=True, default=False,
tooltip="Disable previews for multi-frame latents (video workflows)"),
io.Combo.Input("precision_mode", optional=True, default="auto",
options=["auto", "fp32", "bf16"],
tooltip="ROCm precision hint (no-op on CUDA/CPU)"),
io.Boolean.Input("compatibility_mode", optional=True, default=False,
tooltip="Force pure stock behavior, no ROCm optimizations"),
],
outputs=[
io.Latent.Output(display_name="output"),
io.Latent.Output(display_name="denoised_output"),
]
)
@classmethod
def execute(cls, noise, guider, sampler, sigmas, latent_image,
optimize_for_video=False, precision_mode="auto", compatibility_mode=False) -> io.NodeOutput:
# ── Detect architecture and model type ──────────────────────────
model = guider.model_patcher
if not compatibility_mode:
arch_info = detect_architecture()
model_info = detect_model_sampling_type(model)
apply_rocm_backend_settings(arch_info)
if model_info["has_high_memory"]:
print(f"💾 High-memory model detected (factor: {model_info['memory_usage_factor']}) — cleaning up")
emergency_memory_cleanup()
if model_info["model_type"] == "flow":
print(f"🌊 Flow-matching model detected ({model_info['latent_channels']}ch latent)")
elif model_info["is_pixel_space"]:
print(f"📷 Pixel-space model detected — no VAE decode needed downstream")
is_rocm = bool(getattr(torch.version, 'hip', None))
if is_rocm and torch.cuda.is_available() and precision_mode == "bf16":
try:
if getattr(torch.cuda, 'is_bf16_supported', lambda: False)():
pass
except Exception:
pass
# ── Stock SamplerCustomAdvanced logic ──────────────────────────
latent = latent_image
latent_image_tensor = latent["samples"]
latent = latent.copy()
latent_image_tensor = comfy.sample.fix_empty_latent_channels(
guider.model_patcher, latent_image_tensor,
latent.get("downscale_ratio_spacial", None),
latent.get("downscale_ratio_temporal", None)
)
latent["samples"] = latent_image_tensor
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
x0_output = {}
total_steps = sigmas.shape[-1] - 1
# ── Video optimization ──────────────────────────────────────────
is_video_workflow = False
if not compatibility_mode and optimize_for_video and latent_image_tensor.ndim == 5 and latent_image_tensor.shape[2] > 1:
is_video_workflow = True
# ── Create previewer once (like stock prepare_callback) ──────
previewer = None
if not is_video_workflow:
try:
previewer = latent_preview.get_previewer(
guider.model_patcher.load_device,
guider.model_patcher.model.latent_format
)
except Exception:
pass
pbar = comfy.utils.ProgressBar(total_steps)
last_update_time = [time.time()]
start_time = time.time()
reported_milestones = set()
def enhanced_callback(step, x0, x, total_steps):
if x0 is not None:
x0_output["x0"] = x0
current_time = time.time()
progress_pct = ((step + 1) / total_steps) * 100
pct_key = (int(progress_pct) // 10) * 10
# ── Update progress bar with preview (throttled) ────────
if current_time - last_update_time[0] >= 0.3 or step == total_steps - 1:
preview_bytes = None
if previewer and step % 5 == 0:
try:
preview_bytes = previewer.decode_latent_to_preview_image("JPEG", x0)
except Exception:
pass
try:
pbar.update_absolute(step + 1, total_steps, preview=preview_bytes)
except Exception:
pass
last_update_time[0] = current_time
# ── Console timing at 10% milestones only ──────────────
if step == 0 or step == total_steps - 1 or pct_key not in reported_milestones:
reported_milestones.add(pct_key)
elapsed = current_time - start_time
wf_type = "Video" if is_video_workflow else "Image"
if step == 0:
print(f"📊 ROCm CustomAdvanced: Starting {total_steps} steps...", flush=True)
else:
avg = elapsed / (step + 1)
est_total = elapsed / ((step + 1) / total_steps)
est_remaining = est_total - elapsed
print(f"📊 ROCm CustomAdvanced: {progress_pct:.0f}% | "
f"Step {step + 1}/{total_steps} | "
f"{elapsed:.1f}s elapsed ~{est_remaining:.1f}s remain "
f"({avg:.2f}s/step)", flush=True)
callback = enhanced_callback
disable_pbar = is_video_workflow
# ── Sampling ────────────────────────────────────────────────────
print(f"🚀 ROCm SamplerCustomAdvanced: {total_steps} steps"
f"{' (video mode)' if is_video_workflow else ''}", flush=True)
samples = guider.sample(
noise.generate_noise(latent), latent_image_tensor, sampler, sigmas,
denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar,
seed=noise.seed
)
samples = samples.to(comfy.model_management.intermediate_device())
elapsed = time.time() - start_time
print(f"✅ ROCm SamplerCustomAdvanced: {total_steps} steps in {elapsed:.1f}s "
f"({total_steps / elapsed:.2f} it/s)", flush=True)
# ── Post-sample memory cleanup ──────────────────────────────────
if not compatibility_mode:
try:
model_info = detect_model_sampling_type(model)
if model_info["has_high_memory"]:
gentle_memory_cleanup()
except Exception:
pass
# ── Build output (identical to stock) ──────────────────────────
out = latent.copy()
out.pop("downscale_ratio_spacial", None)
out.pop("downscale_ratio_temporal", None)
out["samples"] = samples
if "x0" in x0_output:
x0_out = guider.model_patcher.model.process_latent_out(x0_output["x0"].cpu())
if samples.is_nested:
latent_shapes = [x.shape for x in samples.unbind()]
x0_out = comfy.nested_tensor.NestedTensor(
comfy.utils.unpack_latents(x0_out, latent_shapes)
)
out_denoised = latent.copy()
out_denoised["samples"] = x0_out
else:
out_denoised = out
return io.NodeOutput(out, out_denoised)
class ROCMSamplerCustomAdvancedBenchmark:
"""
Benchmark node that compares stock SamplerCustomAdvanced vs ROCMSamplerCustomAdvanced.
Runs both samplers with identical inputs and reports timing/memory differences.
Connect this to the same sub-components as SamplerCustomAdvanced.
The node runs stock first, then ROCm-optimized, and outputs a comparison string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"noise": ("NOISE",),
"guider": ("GUIDER",),
"sampler": ("SAMPLER",),
"sigmas": ("SIGMAS",),
"latent_image": ("LATENT",),
},
"optional": {
"optimize_for_video": ("BOOLEAN", {"default": False}),
"precision_mode": (["auto", "fp32", "bf16"], {"default": "auto"}),
}
}
RETURN_TYPES = ("LATENT", "STRING")
RETURN_NAMES = ("LATENT", "BENCHMARK_REPORT")
FUNCTION = "run"
CATEGORY = "ROCm Ninodes/Sampling"
DESCRIPTION = "Compare stock vs ROCm-optimized custom sampler performance"
def run(self, noise, guider, sampler, sigmas, latent_image,
optimize_for_video=False, precision_mode="auto"):
import time as time_module
import gc
latent = latent_image
latent_image_tensor = latent["samples"]
latent = latent.copy()
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
results = {}
# ── Phase 1: Stock behavior ──
print("═══ Phase 1: Stock SamplerCustomAdvanced ═══", flush=True)
torch.cuda.empty_cache()
gc.collect()
mem_before = torch.cuda.memory_allocated(0) if torch.cuda.is_available() else 0
t0 = time_module.time()
stock_samples = guider.sample(
noise.generate_noise(latent), latent_image_tensor, sampler, sigmas,
denoise_mask=noise_mask, disable_pbar=False, seed=noise.seed
)
stock_samples = stock_samples.to(comfy.model_management.intermediate_device())
t1 = time_module.time()
mem_after = torch.cuda.memory_allocated(0) if torch.cuda.is_available() else 0
stock_time = t1 - t0
stock_mem = mem_after - mem_before
results["stock"] = {
"time": stock_time,
"mem_delta": stock_mem,
}
print(f" Stock: {stock_time:.2f}s, memory delta: {stock_mem / 1024**2:.0f}MB", flush=True)
# ── Phase 2: ROCm-optimized ──
print("═══ Phase 2: ROCm SamplerCustomAdvanced ═══", flush=True)
torch.cuda.empty_cache()
gc.collect()
arch_info = detect_architecture()
model = guider.model_patcher
model_info = detect_model_sampling_type(model)
apply_rocm_backend_settings(arch_info)
if model_info["has_high_memory"]:
emergency_memory_cleanup()
is_video_workflow = False
if optimize_for_video and latent_image_tensor.ndim == 5 and latent_image_tensor.shape[2] > 1:
is_video_workflow = True
mem_before_rocm = torch.cuda.memory_allocated(0) if torch.cuda.is_available() else 0
total_steps = sigmas.shape[-1] - 1
pbar = comfy.utils.ProgressBar(total_steps)
last_update = [time_module.time()]
x0_output = {}
def rocm_callback(step, x0, x, total_steps):
if x0 is not None:
x0_output["x0"] = x0
now = time_module.time()
if now - last_update[0] >= 0.3 or step == total_steps - 1:
try:
pbar.update_absolute(step + 1, total_steps)
except Exception:
pass
last_update[0] = now
t2 = time_module.time()
rocm_samples = guider.sample(
noise.generate_noise(latent), latent_image_tensor, sampler, sigmas,
denoise_mask=noise_mask, callback=rocm_callback,
disable_pbar=is_video_workflow, seed=noise.seed
)
rocm_samples = rocm_samples.to(comfy.model_management.intermediate_device())
t3 = time_module.time()
mem_after_rocm = torch.cuda.memory_allocated(0) if torch.cuda.is_available() else 0
rocm_time = t3 - t2
rocm_mem = mem_after_rocm - mem_before_rocm
results["rocm"] = {
"time": rocm_time,
"mem_delta": rocm_mem,
}
if model_info["has_high_memory"]:
gentle_memory_cleanup()
print(f" ROCm: {rocm_time:.2f}s, memory delta: {rocm_mem / 1024**2:.0f}MB", flush=True)
# ── Build report ──
time_speedup = (stock_time / rocm_time * 100) - 100 if rocm_time > 0 else 0
mem_diff = stock_mem - rocm_mem
report = (
f"Benchmark Results\n"
f"{'─' * 50}\n"
f"Stock: {stock_time:.2f}s, {stock_mem / 1024**2:.0f}MB peak\n"
f"ROCm: {rocm_time:.2f}s, {rocm_mem / 1024**2:.0f}MB peak\n"
f"Speed: {'+' if time_speedup >= 0 else ''}{time_speedup:.1f}%\n"
f"Memory: {mem_diff / 1024**2:+.0f}MB delta\n"
f"Steps: {total_steps}\n"
f"Model: {model_info['model_type']} ({model_info['latent_channels']}ch, mem factor {model_info['memory_usage_factor']}x)\n"
f"GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}\n"
f"{'─' * 50}\n"
)
out = latent.copy()
out["samples"] = rocm_samples
return (out, report)