Optimize noise nodes with GPU-accelerated tensors - #7
Open
djdarcy wants to merge 6 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
System specs where issue observed:
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:
outimage.putpixel((x,y), (nr,ng,nb))loops withtorch.randint()vectorized operationscomfy.model_management.get_torch_device()Nodes Optimized:
Code Reduction:
Implementation details
Device Selection Pattern:
Falls back to CPU if ComfyUI device management unavailable.
Vectorized Generation (RandNoise example):
PlasmaNoise Output Stage (new):
Seed Management:
Maintains reproducibility within new implementation.
Edge Case Handling (GreyNoise):
Performance results
Testing (Win11 + RTX 5090):
Improvement achieved by:
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 PyTorchtorch.randint()for GPU acceleration.Impact:
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:
Not tested:
No formal benchmarks: Performance improvement based on personal observation and operation count analysis, not precise timing measurements.
Scope limitations
Not included:
Unknown factors
Unclear:
Potential issues:
Changes
nodes.py
Dependencies
Technical notes
Important torch.randint() Difference:
ComfyUI IMAGE format:
Memory efficiency:
PlasmaNoise Hybrid Approach:
Edit to add after some more experimenting
After testing, confirmed:
No crashes, errors, or visual artifacts observed. Parameter controls all functional.
Questions / feedback welcome
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!