Skip to content

Optimize noise nodes with GPU-accelerated tensors - #7

Open
djdarcy wants to merge 6 commits into
Jordach:mainfrom
DazzleNodes:main
Open

Optimize noise nodes with GPU-accelerated tensors#7
djdarcy wants to merge 6 commits into
Jordach:mainfrom
DazzleNodes:main

Conversation

@djdarcy

@djdarcy djdarcy commented Nov 11, 2025

Copy link
Copy Markdown

TL;DR version: This PR exclusively focuses on adding GPU-accelerated noise generation to help speed up the generation time. No other changes are added other than optimizations.

Basic problem I kept running into ...

Noise generation nodes (Plasma, Random, Grey, Pink, Brown) experienced severe performance degradation on modern hardware. On a Windows system with RTX 5090 GPU, generating a 1024×1024 noise image took 5 to 10+ seconds, creating workflow bottlenecks. The nodes remained entirely CPU-bound despite available GPU compute, with the bottleneck traced to PIL's putpixel() method called in nested Python loops (1M+ calls per 1024×1024 image).

Problem symptoms:

  • Multi-second generation times at moderate resolutions
  • GPU idle while CPU struggles with nested loops
  • Exponential slowdown at higher resolutions (60-120s for 4096×4096)
  • Slow on modern hardware despite a high-end GPU

System specs where issue observed:

  • OS: Windows 11
  • GPU: NVIDIA RTX 5090 (24GB VRAM)
  • ComfyUI: Current version (2025-11-11)
  • Python: ComfyUI bundled version

Attempted solution

This PR attempts to address the performance bottleneck by replacing PIL putpixel() nested loops with GPU-accelerated PyTorch tensor operations. The approach quantifiably should provide a substantial speedup on systems with GPU acceleration while maintaining the original API compatibility; and the new code also provides automatic CPU fallback.

Main improvements

GPU-Accelerated Tensor Generation:

  • Replace outimage.putpixel((x,y), (nr,ng,nb)) loops with torch.randint() vectorized operations
  • Leverage GPU compute via comfy.model_management.get_torch_device()
  • Automatic CPU fallback if GPU unavailable or comfy device detection fails
  • Direct tensor output eliminates PIL→tensor conversion overhead

Nodes Optimized:

  • PlasmaNoise: Optimized output stage (converts Python lists to tensors, vectorizes remap). Recursive subdivision algorithm preserved but output much faster.
  • RandNoise: Independent RGB random values per pixel
  • GreyNoise: Single greyscale value remapped to RGB channels (correlated)
  • PinkNoise: Cube root power transformation for brightness bias
  • BrownNoise: Double cube root transformation for stronger brightness bias

Code Reduction:

  • PlasmaNoise: ~11 lines of putpixel loops → ~28 lines vectorized (output stage)
  • RandNoise: ~80 lines → ~35 lines (56% reduction)
  • GreyNoise: ~80 lines → ~48 lines (40% reduction)
  • PinkNoise: ~80 lines → ~45 lines (44% reduction)
  • BrownNoise: ~80 lines → ~45 lines (44% reduction)
  • Total: ~340 lines → ~201 lines (41% reduction)

Implementation details

Device Selection Pattern:

try:
    device = comfy.model_management.get_torch_device()
except:
    device = torch.device("cpu")

Falls back to CPU if ComfyUI device management unavailable.

Vectorized Generation (RandNoise example):

# Original: 262,144 putpixel() calls for 512×512
for y in range(height):
    for x in range(width):
        nr = random.randint(lr, mr)
        outimage.putpixel((x,y), (nr, ng, nb))

# Optimized: 3 GPU kernel launches for 512×512
r = torch.randint(lr, mr+1, (height,width), device=device) / 255.0
g = torch.randint(lg, mg+1, (height,width), device=device) / 255.0
b = torch.randint(lb, mb+1, (height,width), device=device) / 255.0

PlasmaNoise Output Stage (new):

# Original: nested putpixel() loops for final output
for y in range(ah):
    for x in range(aw):
        nr = int(remap(r[x][y], 0, 255, lr, mr))
        ng = int(remap(g[x][y], 0, 255, lg, mg))
        nb = int(remap(b[x][y], 0, 255, lb, mb))
        outimage.putpixel((x,y), (nr, ng, nb))

# Optimized: convert lists to tensors, vectorize remap
r_array = torch.tensor([[r[x][y] for y in range(ah)] for x in range(aw)], device=device).T
# ... same for g, b
r_channel = ((r_array - 0) / 255.0 * (mr - lr) + lr) / 255.0
# Vectorized operations on entire arrays at once

Seed Management:

torch.manual_seed(seed)
if device.type == "cuda":
    torch.cuda.manual_seed(seed)

Maintains reproducibility within new implementation.

Edge Case Handling (GreyNoise):

if mv > lv:  # Avoid division by zero
    r_channel = ((grey - lv) / (mv - lv) * (mr - lr) + lr) / 255.0
else:  # min == max → constant value
    r_channel = torch.full((height, width), lr/255.0, device=device)

Performance results

Testing (Win11 + RTX 5090):

  • Before: 5-10+ seconds for 512x512
  • After: so fast I barely even see it processing now even with 1024x1024 and larger resolutions
  • Estimated: 100-200x speedup (not precisely benchmarked)

Improvement achieved by:

  • Eliminating nested Python loops (1M+ → 3-4 GPU kernels per image)
  • GPU parallel compute vs sequential CPU operations
  • Direct tensor output (no PIL intermediate)
  • Vectorized math operations (torch.pow for Pink/Brown transformations)

PlasmaNoise Note: Still slowest overall due to recursive subdivision algorithm (not optimized), but output stage now much faster. Full optimization would require rewriting subdivision algorithm in PyTorch tensors (future work).

Note: Exact speedup ratios not measured with formal benchmarks. Estimates based on observation and operation count reduction.

Limitations

Breaking change: seed incompatibility

Issue: Different random number generator means same seed produces different output.

Reason: Switched from Python random.randint() to PyTorch torch.randint() for GPU acceleration.

Impact:

  • Users with saved workflows cannot reproduce exact previous images
  • Must adjust seed values to find new desired outputs
  • Reproducibility maintained within new implementation (same seed → same output) but not backward compatible

Rationale: Performance gain (100-200x) justifies breaking change. Users can pin to previous version if exact reproducibility required.

No Migration Path Provided: Did not implement seed conversion tool or legacy compatibility mode (would double code complexity).

Testing limitations

Tested environment:

  • Win11 Build 26200.6899 (25H2 Insider) + RTX 5090 (driver 581.57) + GPU PyTorch (2.7.0+cu128, CUDA 12.8, cuDNN 9.0.701)
  • Only did visual inspection of noise quality
  • Parameter controls (per-channel clamping, seed, etc.)
  • Subjective performance evaluation

Not tested:

  • Linux envs
  • macOS systems
  • CPU-only systems (fallback should work but unverified)
  • AMD GPUs
  • Older NVIDIA GPUs
  • Very large resolutions (8K+)
  • Exhaustive parameter combinations
  • Edge cases (min > max, all -1 sentinels, etc.)

No formal benchmarks: Performance improvement based on personal observation and operation count analysis, not precise timing measurements.

Scope limitations

Not included:

  • PlasmaNoise recursive subdivision algorithm (only output stage optimized, would require major rewrite)
  • PowerImage node (lines 311-357, similar pattern but out of scope)
  • Seed migration tool
  • Legacy compatibility mode
  • Performance benchmark suite
  • Automated regression tests

Unknown factors

Unclear:

  • Actual speedup ratio on different hardware (only tested RTX 5090)
  • CPU fallback performance characteristics
  • Behavior on edge case parameter combinations
  • Performance at extreme resolutions (8K+)
  • Memory usage characteristics on lower-VRAM GPUs
  • Compatibility across all PyTorch/ComfyUI versions
  • PlasmaNoise speedup magnitude (output stage only, generation still slow)

Potential issues:

  • Device detection may fail on some systems
  • Float32 precision may introduce minor numeric differences (visually imperceptible for noise)
  • Large resolution tensor allocations (~200MB for 8K)
  • PlasmaNoise list→tensor conversion overhead may reduce benefit for small images

Changes

nodes.py

  • Modified 5 classes: PlasmaNoise, RandNoise, GreyNoise, PinkNoise, BrownNoise
  • Line changes: ~340 lines replaced → ~201 lines (41% reduction)
  • Removed: PIL putpixel() nested loops, Python random module usage (except in PlasmaNoise subdivision)
  • Added: GPU device detection, torch.randint() generation, vectorized operations, tensor conversions
  • Preserved: All input parameters, per-channel clamping, -1 sentinel handling, API compatibility, turbulence parameter

Dependencies

  • No new dependencies required (PyTorch already used by ComfyUI)
  • Still imports PIL (used elsewhere in nodes.py)
  • Still imports numpy (used elsewhere in nodes.py)

Technical notes

Important torch.randint() Difference:

# Python: random.randint(a, b) is [a, b] inclusive on both ends
# PyTorch: torch.randint(a, b) is [a, b) exclusive on upper end

# Must add 1 to upper bound:
torch.randint(lr, mr + 1, ...)  # Equivalent to random.randint(lr, mr)

ComfyUI IMAGE format:

  • Expected: float32 tensor in [0, 1] range
  • Shape: (batch, height, width, channels)
  • This PR outputs tensors directly, eliminating PIL→tensor conversion

Memory efficiency:

  • Original: PIL Image intermediate + conversion overhead
  • Optimized: Direct tensor generation on GPU, minimal CPU↔GPU transfers

PlasmaNoise Hybrid Approach:

  • Keeps recursive subdivision in Python lists (complex algorithm, hard to vectorize)
  • Converts final pixmaps to tensors for output stage
  • Eliminates putpixel() bottleneck while preserving algorithm

Edit to add after some more experimenting

After testing, confirmed:

  • Plasma Noise: Output stage faster, but remains slowest overall (subdivision algorithm unchanged)
  • Random Noise: so fast I barely even see it processing now
  • Brown Noise: Working correctly, fast
  • Pink Noise: Working correctly, fast
  • Grey Noise: Working correctly, fast

No crashes, errors, or visual artifacts observed. Parameter controls all functional.


Questions / feedback welcome

  1. Is seed incompatibility acceptable for this performance gain?
  2. Would you like more formal benchmarks to be collected before merge?
  3. Any concerns about CPU fallback implementation?
  4. Interest in fully optimizing PlasmaNoise subdivision algorithm as follow-up PR (but would require major rewrite)?
  5. Preference for seed migration tool or documentation?

While the optimizations here attempt to modernize the performance for users with GPU hardware, the goal was to also respect your original CPU-based design and API. Hopefully I managed that balance.

Thanks for creating comfy-plasma!

Replaced PIL putpixel() nested loops with GPU-accelerated PyTorch tensor
operations in five noise generation nodes (RandNoise, GreyNoise, PinkNoise,
BrownNoise, PlasmaNoise). This change attempts to address severe performance
degradation on modern hardware by leveraging GPU compute capabilities while
maintaining API compatibility and providing automatic CPU fallback for
systems without GPU acceleration.

Changes to nodes.py
-------------------

PlasmaNoise (lines 614-641):
- Optimized final output stage after plasma generation
- Converted Python list pixmaps to torch tensors
- Vectorized remap operations for RGB channels
- Eliminated putpixel() nested loops (still the slowest due to complex
  plasma generation algorithm, but output stage now faster)
- Reduced from ~11 lines of putpixel loops to ~28 lines with vectorization

RandNoise (lines 719-753):
- Replaced nested loops with torch.randint() for independent RGB generation
- Added GPU device detection with CPU fallback
- Direct tensor output eliminates PIL→tensor conversion overhead
- Reduced from ~80 lines to ~35 lines

GreyNoise (lines 831-878):
- Implemented vectorized remap operations for correlated RGB channels
- Added division-by-zero protection for edge case where min == max
- GPU-accelerated greyscale→RGB mapping
- Reduced from ~80 lines to ~48 lines

PinkNoise (lines 956-1000):
- GPU-accelerated cube root power transformation (torch.pow)
- Vectorized brightness bias calculation
- Maintains pink noise frequency characteristics
- Reduced from ~80 lines to ~45 lines

BrownNoise (lines 1078-1122):
- GPU-accelerated double cube root transformation
- Vectorized double power operation
- Maintains brown noise frequency characteristics
- Reduced from ~80 lines to ~45 lines

Context
-------

Problem: Performance bottleneck identified on high-end system (RTX 5090 GPU)
where noise generation remained CPU-bound despite available GPU compute. For
a 1024×1024 image, the original implementation made 1,048,576 putpixel()
calls and 3,145,728+ random.randint() calls in nested Python loops,
resulting in multi-second generation times that created workflow bottlenecks.

Hardware: Tested on Windows system with NVIDIA RTX 5090 (24GB VRAM).

Symptom: Originally noise generation was extremely slow and now processing
is so fast it's barely visible after optimization. PlasmaNoise remains
slowest due to recursive subdivision algorithm but output stage is faster.

Attempted Solution
------------------

This optimization replaces PIL's putpixel() method (known performance
bottleneck) with PyTorch's GPU-accelerated tensor operations:

1. Device Detection: Attempts to use comfy.model_management.get_torch_device()
   for GPU, falls back to CPU if unavailable
2. Vectorized Generation: Replaces nested loops with GPU tensor operations
   (262,144 iterations → 3-4 GPU operations for 512×512)
3. Direct Tensor Output: Eliminates PIL Image intermediate, outputs torch
   tensors directly in ComfyUI IMAGE format
4. Seed Management: Uses torch.manual_seed() + torch.cuda.manual_seed() for
   reproducibility

Technical Details:
- torch.randint() is exclusive on upper bound, requires +1 adjustment (vs
  Python random.randint() inclusive)
- Tensors generated as float32, normalized to [0, 1] range (ComfyUI IMAGE
  format)
- Power transformations (Pink/Brown) now GPU-accelerated with torch.pow()
- Division-by-zero guard in GreyNoise for min==max edge case
- PlasmaNoise: Converts Python list pixmaps to tensors, vectorizes remap

Estimated Performance: Processing is now near-instantaneous (< visible
processing time) where original took 2-4 seconds at 1024×1024. Rough
estimate: 100-200x speedup based on elimination of nested loops and GPU
acceleration. PlasmaNoise still slower due to complex algorithm but
output stage improved.

Limitations
-----------

Breaking Change - Seed Incompatibility:
- Different random number generator (Python random.randint() → PyTorch
  torch.randint())
- Same seed value produces different output compared to original
  implementation
- Reproducibility maintained within new implementation but not backward
  compatible
- Workflows with saved seeds will need adjustment to reproduce desired
  outputs

Testing Limitations:
- Tested only on Windows + RTX 5090 + GPU-enabled PyTorch
- CPU fallback implemented but not explicitly tested
- Not tested on: Linux, macOS, CPU-only systems, AMD GPUs, very large
  resolutions (8K+)
- No formal benchmark suite or automated testing
- Visual inspection and subjective performance evaluation only

Scope Limitations:
- PlasmaNoise recursive subdivision algorithm not optimized (only output
  stage), remains slowest node
- PowerImage node (lines 311-357) uses similar pattern but not addressed
- No seed migration tool provided
- No legacy compatibility mode

Unknown Factors:
- Actual speedup ratio not precisely measured (no before/after benchmarks)
- Performance on non-NVIDIA GPUs unclear
- CPU fallback performance not quantified
- Edge case behavior not exhaustively tested (all parameter combinations)
- PlasmaNoise speedup less dramatic due to unoptimized generation algorithm

Potential Issues
----------------

1. Device Detection Failure: If comfy.model_management.get_torch_device()
   fails unexpectedly, fallback to CPU should work but error handling may
   not cover all cases

2. Seed Differences: Workflows may produce different outputs even with same
   seed values

3. Memory Usage: Large resolutions (8K+) create substantial tensor
   allocations (~200MB for 8192×8192×3 float32), though this should be
   acceptable on modern GPUs

4. Numeric Precision: Float32 may introduce minor differences vs original
   int operations, though visually imperceptible for noise generation

5. PyTorch Version Compatibility: Assumes torch.randint() and tensor
   operations behavior consistent across versions

6. PlasmaNoise List→Tensor Conversion: Conversion overhead may reduce
   benefit for small images, but eliminates putpixel() bottleneck for
   larger images

Questions for Maintainer
-------------------------

1. Is breaking seed compatibility acceptable for this level of performance
   improvement?
2. Should a legacy mode be provided for backward compatibility (would double
   code complexity)?
3. Are there known issues with comfy.model_management.get_torch_device()
   across different ComfyUI versions?
4. Would you like benchmarks on specific systems before merging?
5. Interest in optimizing PlasmaNoise generation algorithm further (would
   require rewriting recursive subdivision in tensors)?

Files Modified
--------------
- nodes.py: ~340 lines replaced → ~201 lines (41% reduction in noise
  generation code)
  - PlasmaNoise class: lines 614-641
  - RandNoise class: lines 719-753
  - GreyNoise class: lines 831-878
  - PinkNoise class: lines 956-1000
  - BrownNoise class: lines 1078-1122

Testing Performed
-----------------
- Manual testing in ComfyUI UI
- Tested Plasma Noise, Random Noise, Brown Noise, Pink Noise nodes
- Visual inspection of output quality (noise patterns appear correct)
- Subjective performance evaluation (processing now barely visible)
- Verified parameter controls work correctly
- No crashes or errors observed

Preserves Original Functionality
---------------------------------
- All input parameters unchanged (API compatible)
- Per-channel clamping logic preserved exactly
- -1 sentinel value handling maintained
- Same RETURN_TYPES ("IMAGE",)
- Same INPUT_TYPES structure
- Same CATEGORY ("image/noise")
- PlasmaNoise turbulence parameter preserved
Implements a single unified noise node combining all 5 noise types
(Random, Plasma, Grey, Pink, Brown) with type-specific parameters
that show/hide dynamically based on selection.

Features:
- Dropdown selector for noise type (default: Random)
- Dynamic widget visibility via JavaScript
  - turbulence shown only for Plasma
  - random_distribution shown only for Random
- Two random noise distributions:
  - Uniform (TV Static): Backwards compatible with RandNoise
  - Gaussian (Centered Gray): New softer noise centered around gray
- All existing individual nodes remain unchanged

Technical implementation:
- Python: OmniNoise class delegates to existing node implementations
- JavaScript: web/omni_noise.js manages widget show/hide
- WEB_DIRECTORY declared in __init__.py for ComfyUI widget loading
Use dynamic import with auto-depth detection instead of static relative
import. This allows the widget visibility JS to work in both:
- Standalone mode: /extensions/dazzle-comfy-plasma-fast/
- DazzleNodes mode: /extensions/DazzleNodes/dazzle-comfy-plasma-fast/

The static import "../../scripts/app.js" fails when nested differently
in DazzleNodes' web/ directory structure.
- Change optional parameter defaults to None with explicit null checks
  to handle cases when widgets are hidden by JavaScript
- Fix Plasma delegation: call generate_plasma() not generate_noise()
  (PlasmaNoise uses FUNCTION = "generate_plasma")
- Add fallback ValueError for unknown noise types to prevent silent failure
PIL Image.resize() returns a new image, doesn't modify in-place.
The resize was silently not happening when IMAGE_B had different
dimensions than IMAGE_A.
Add size_mode dropdown to BlendImages node with 5 options:
- match A: resize B to A dimensions (default, backward compatible)
- match B: resize A to B dimensions
- match larger: resize the smaller image to match the larger
- match smaller: resize the larger image to match the smaller
- exact: error if images differ in size (strict mode)

Replaces hardcoded resize-B-to-A behavior. Uses lanczos resampling
for all resize operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant