Files
pollockjj-ComfyUI-MultiGPU/memory-bank/systemPatterns.md
T
John Pollock 429be7c912 docs: update activeContext for v2.5.0 release with refactoring summary
Update active context documentation to reflect v2.5.0 release candidate status.
Major achievements documented:

- DisTorch2 allocation refactoring (-179 lines, unified UNET/CLIP logic)
- Production cleanup removing debug instrumentation (-40 lines)
- Verified selective unload system working with production logs
- Architecture status showing all core files production-ready
- Updated memory management pipeline with verification details

Reorganized content to prioritize recent session achievements (2025-09-30)
and production readiness status. Total code reduction: 219 lines through
consolidation and cleanup while maintaining full functionality.
2025-09-30 09:07:05 -05:00

16 KiB
Raw Blame History

System Architecture & Patterns (Updated 2025-09-29)

Core Architecture

Dynamic Class Override System

Foundation Pattern: City96's elegant inheritance-based approach (Dec 2024 revolution)

def override_class(original_class, device_param="device"):
    class MultiGPUClass(original_class):
        @classmethod
        def INPUT_TYPES(cls):
            inputs = original_class.INPUT_TYPES()
            inputs["required"][device_param] = (get_device_list(),)
            return inputs
            
        def override(self, *args, **kwargs):
            device = kwargs.pop(device_param, None)
            mm.text_encoder_device = device
            return original_class.FUNCTION(self, *args, **kwargs)
            
    return MultiGPUClass

Key Benefits:

  • 50 lines vs 400+: Eliminated manual class definitions
  • Universal Support: Works with any ComfyUI loader node
  • Maintenance: Auto-adapts to ComfyCore changes
  • Consistency: Unified behavior across all MultiGPU nodes

Load-Patch-Distribute (LPD) Method

DisTorch2 Core Process:

# 1. LOAD - Always on compute device first
tensor = load_tensor_on_compute_device(tensor_name)

# 2. PATCH - Apply all LoRAs at full precision
if lora_patches:
    tensor = apply_lora_patches(tensor, lora_patches, precision=torch.float16)

# 3. DISTRIBUTE - Move to target device after patching
final_tensor = tensor.to(target_device)

Design Principles:

  • Quality First: No precision loss during LoRA application
  • Deterministic: Same allocation every time
  • ComfyUI Native: Works with existing ComfyCore patterns

Memory Management Architecture

Virtual VRAM System

Concept: Make CPU/secondary GPU memory appear as extended VRAM

class VirtualVRAM:
    def __init__(self, compute_device, donor_device, virtual_gb):
        self.compute_device = compute_device  # e.g., "cuda:0"
        self.donor_device = donor_device      # e.g., "cpu" or "cuda:1"
        self.virtual_gb = virtual_gb          # Extended memory pool
        
    def allocate_layers(self, model_layers, allocation_string):
        # "cuda:0,2.5gb;cpu,*" -> assign layers based on cumulative memory

Expert Allocation Modes

Bytes Mode (Recommended):

# "cuda:0,2.5gb;cuda:1,3.0g;cpu,*"
def parse_bytes_allocation(allocation_string):
    devices = []
    for device_spec in allocation_string.split(';'):
        device_name, memory_spec = device_spec.split(',')
        if memory_spec == '*':
            memory_bytes = float('inf')  # Overflow device
        else:
            memory_bytes = parse_memory_string(memory_spec)  # 2.5gb -> bytes
        devices.append((device_name, memory_bytes))
    return devices

Ratio Mode (llama.cpp style):

# "cuda:0,25%;cpu,75%" -> 1:3 split
def parse_ratio_allocation(allocation_string):
    total_ratio = sum(float(spec.split(',')[1].rstrip('%')) for spec in allocation_string.split(';'))
    device_ratios = []
    for device_spec in allocation_string.split(';'):
        device_name, ratio_spec = device_spec.split(',')
        ratio = float(ratio_spec.rstrip('%')) / total_ratio
        device_ratios.append((device_name, ratio))
    return device_ratios

Selective Ejection Pipeline (v2.5.0 - VERIFIED WORKING)

Load-time Flagging (per-model transient):

# In DisTorch2 wrapper after real loader returns
if hasattr(out[0], 'model') and hasattr(out[0].model, '_mgpu_keep_loaded'):
    keep_loaded = out[0].model._mgpu_keep_loaded
    out[0].model._mgpu_unload_distorch_model = (not keep_loaded)

Purpose: Mark specific DisTorch models for ejection when user unchecks "keep loaded"

Manager-Parity Cleanup Trigger:

def force_full_system_cleanup(reason="manual", force=True):
    pq.set_flag("unload_models", True)  # Exactly what Manager's
    pq.set_flag("free_memory", True)    # "Free model and node cache" does

Selective Unloading (patched mm.unload_all_models):

def _mgpu_patched_unload_all_models():
    # Categorize models by flag
    models_to_unload = [lm for lm in mm.current_loaded_models 
                        if getattr(lm.model, '_mgpu_unload_distorch_model', False)]
    kept_models = [lm for lm in mm.current_loaded_models
                   if not getattr(lm.model, '_mgpu_unload_distorch_model', False)]
    
    if kept_models:
        # Selective unload: eject flagged, retain others
        for lm in models_to_unload:
            lm.model_unload(unpatch_weights=True)
        
        # Add GC anchors to prevent premature collection
        for lm in kept_models:
            add_retention_anchor(lm.model, "keep_loaded_protection")
        
        # Rebuild with kept models only
        mm.current_loaded_models = kept_models
    else:
        # No models to keep - standard cleanup
        _mgpu_original_unload_all_models()

Multi-Device VRAM + CPU Management (patched mm.soft_empty_cache):

def soft_empty_cache_distorch2_patched(force=False):
    # 1. Detect DisTorch2 activity
    is_distorch_active = any(model_hash in safetensor_allocation_store 
                            for model in mm.current_loaded_models)
    
    # 2. VRAM allocator management
    if is_distorch_active:
        soft_empty_cache_multigpu()  # Clear all device caches
    else:
        original_soft_empty_cache(force)  # Standard single-device
    
    # 3. Adaptive CPU memory management
    check_cpu_memory_threshold()
    
    # 4. Forced executor reset (Manager parity)
    if force:
        trigger_executor_cache_reset(reason="forced_soft_empty", force=True)

Verified Working (Production Logs 2025-09-30):

[CATEGORIZE_SUMMARY] kept_models: 2, models_to_unload: 1, total: 3
[SELECTIVE_UNLOAD] Proceeding with selective unload: retaining 2, unloading 1
[UNLOAD_EXECUTE] Unloading model: Flux
[REMAINING_MODEL] 0: AutoencodingEngine  
[REMAINING_MODEL] 1: FluxClipModel_

Device Detection & Management

Multi-Device Enumeration:

def get_device_list():
    devices = ["cpu"]  # Always available
    if torch.cuda.is_available():
        devices.extend([f"cuda:{i}" for i in range(torch.cuda.device_count())])
    # XPU/NPU/MLU/MPS/DirectML/CoreX detection...
    return devices

Device Bandwidth Intelligence (from benchmarking):

  1. NVLINK (~50.8 GB/s)
  2. PCIe 4.0 x16 (~27.2 GB/s)
  3. PCIe 3.0 x8 (~6.8 GB/s)
  4. PCIe 3.0 x4 (~2.1 GB/s)

Integration Patterns

ComfyCore Alignment

Philosophy: Work WITH ComfyUI, not against it

# GOOD: Use ComfyCore's device management
current_device = mm.get_torch_device()
mm.text_encoder_device = target_device

# AVOID: Direct PyTorch device manipulation
torch.cuda.set_device(device_id)  # Bypasses ComfyCore

Node Registration System

# Dynamic registration based on available dependencies
if "ComfyUI-GGUF" in installed_modules:
    NODE_CLASS_MAPPINGS["UnetLoaderGGUFDisTorch2MultiGPU"] = create_gguf_distorch_node()

Dependency Detection

def check_module_availability(module_paths):
    for path in module_paths:
        if os.path.exists(os.path.join(custom_nodes_dir, path)):
            return True
    return False

Performance Optimization Patterns

Layer Transfer Optimization

def optimized_layer_transfer(layer, source_device, target_device):
    if source_device == target_device:
        return layer
    non_blocking = "cuda" in source_device and "cuda" in target_device
    if source_device == "cpu" and "cuda" in target_device:
        layer = layer.pin_memory()
    return layer.to(target_device, non_blocking=non_blocking)

Memory Pressure Management

def should_auto_offload(model_size_gb, vram_available_gb, threshold=0.9):
    return model_size_gb > (vram_available_gb * threshold)

def calculate_offload_amount(model_size_gb, target_vram_usage_gb):
    return max(0, model_size_gb - target_vram_usage_gb)

Error Handling Philosophy

Fail Loudly Pattern

# GOOD: Let ComfyCore changes surface immediately
def load_model(self, model_name, device):
    return original_loader.load_unet(model_name, device)

# AVOID: Defensive coding that masks issues
try:
    return original_loader.load_unet(model_name, device)
except AttributeError:
    return fallback_method()

Integration Validation

def validate_comfycore_integration():
    required_attrs = ['FUNCTION', 'INPUT_TYPES', 'RETURN_TYPES']
    for attr in required_attrs:
        if not hasattr(target_class, attr):
            raise AttributeError(f"ComfyCore node missing {attr} - API changed")

Code Style Patterns

Self-Documenting Code

def override_class_with_device_selection(original_class, device_param_name="device"):
    compute_device = kwargs.get(device_param_name, mm.get_torch_device())

Minimal Comments Philosophy

Prefer structure and naming to convey intent; use comments for non-obvious constraints/assumptions.

Architectural Decision Records

Why Dynamic Class Override vs Manual Definitions

Decision: Use inheritance-based class override (City96 approach) Rationale:

  • Reduces code from 400+ lines to ~50 lines
  • Auto-adapts to ComfyCore changes
  • Eliminates maintenance burden of manual node definitions
  • Provides consistent behavior across all node types

Why Load-Patch-Distribute vs Direct Distribution

Decision: Always load on compute device first, then distribute Rationale:

  • Ensures LoRA patches applied at full precision
  • Maintains quality parity with single-GPU workflows
  • Predictable behavior regardless of target device
  • Works with ComfyCore’s existing patching mechanisms

Why Expert Modes vs Automatic Only

Decision: Provide both automatic and expert allocation modes Rationale:

  • Automatic mode enables low-VRAM users immediately
  • Expert modes allow optimization for specific hardware
  • Performance depends on bandwidth topology; experts need control

Why Universal Device Support vs CUDA-Only

Decision: Support CPU, XPU, NPU, MLU, MPS, DirectML alongside CUDA Rationale:

  • ComfyUI’s user base spans diverse hardware
  • Future-proof for emerging accelerators
  • Hardware democracy principle

Why Per-Model Flag vs Global Sentinel (Updated)

Decision: Use per-model _mgpu_unload_distorch_model instead of a global “DISTORCH2_UNLOAD_MODEL” sentinel Rationale:

  • Surgical precision at model granularity
  • No persistent or cross-workflow state
  • Cleaner semantics under ComfyUI’s queue/flag model

Hardened unloading rule (target to re-apply):

  • If no models are flagged for ejection, mm.unload_all_models must be a strict no-op to preserve retained models across the full Manager-parity flow.

Testing & Validation Patterns

Hardware Configuration Testing

HARDWARE_CONFIGS = [
    {"compute": "cuda:0", "donor": "cpu", "connection": "PCIe 4.0 x16"},
    {"compute": "cuda:0", "donor": "cuda:1", "connection": "NVLink"},
    {"compute": "cuda:0", "donor": "cuda:1", "connection": "PCIe 3.0 x8"},
    {"compute": "cuda:0", "donor": "cuda:1", "connection": "PCIe 3.0 x4"},
]

Model Compatibility Validation

TEST_MODELS = [
    {"name": "FLUX.1-dev", "format": ".safetensors", "size_gb": 23.8},
    {"name": "WAN 2.2", "format": ".safetensors", "size_gb": 14.0},
    {"name": "FLUX-GGUF", "format": ".gguf", "size_gb": 11.8},
    {"name": "QWEN Image", "format": ".safetensors", "size_gb": 38.0},
]

Performance Regression Testing

def benchmark_allocation_performance(model, hardware_config, allocation_configs):
    baseline_time = benchmark_single_gpu(model)
    for allocation in allocation_configs:
        distributed_time = benchmark_distributed(model, hardware_config, allocation)
        performance_ratio = distributed_time / baseline_time
        assert performance_ratio < expected_slowdown_threshold(hardware_config)

Recent Refactorings (v2.5.0)

DisTorch2 Allocation Consolidation (-179 lines)

Problem: 85% code duplication between analyze_safetensor_loading() and analyze_safetensor_loading_clip()

Solution: Unified function with CLIP support flag

def _extract_clip_head_blocks(raw_block_list, compute_device):
    """Helper: Identify and pre-assign CLIP head blocks to compute device"""
    head_keywords = ['embed', 'wte', 'wpe', 'token_embedding', 'position_embedding']
    head_blocks = []
    distributable_blocks = []
    block_assignments = {}
    
    for module_size, module_name, module_object, params in raw_block_list:
        if any(kw in module_name.lower() for kw in head_keywords):
            head_blocks.append((module_size, module_name, module_object, params))
            block_assignments[module_name] = compute_device
        else:
            distributable_blocks.append((module_size, module_name, module_object, params))
    
    return head_blocks, distributable_blocks, block_assignments, head_memory

def analyze_safetensor_loading(model_patcher, allocations_string, is_clip=False):
    """Unified allocation function with CLIP head preservation support"""
    # Common allocation logic...
    
    if is_clip:
        head_blocks, distributable_raw, block_assignments, head_memory = \
            _extract_clip_head_blocks(raw_block_list, compute_device)
        # Adjust compute_device quota for head blocks
        donor_quotas[compute_device] -= head_memory
    else:
        distributable_raw = raw_block_list
        block_assignments = {}
    
    # Continue with unified distribution logic...

Benefits:

  • Single source of truth for allocation
  • CLIP special case isolated in 20-line helper
  • Easier to maintain and debug
  • Same behavior, cleaner architecture

Production Cleanup (-40 lines)

Removed: Diagnostic instrumentation wrapper _mgpu_instrumented_soft_empty_cache()

Rationale: Pure debug logging with no production function - removed to clean codebase

Result: Clear separation between device_utils.py (functional) and model_management_mgpu.py (lifecycle)

Module Architecture (Post-Refactoring)

Core Module Separation

Problem Solved: Eliminated circular import device_utils.py ↔ distorch_2.py

Solution: model_management_mgpu.py as central model lifecycle hub

Module Responsibilities

device_utils.py (Base Layer):

  • Device enumeration and detection
  • VRAM cache management (soft_empty_cache_multigpu)
  • Pure hardware abstraction – no model tracking

model_management_mgpu.py (Core Layer):

  • Model lifecycle tracking and memory logging
  • Cleanup orchestration (force_full_system_cleanup, trigger_executor_cache_reset, check_cpu_memory_threshold)
  • Patched unload path (selective ejection)

distorch_2.py/distorch.py (Feature Layer):

  • DisTorch distribution algorithms and allocation analysis
  • Per-model flagging (_mgpu_unload_distorch_model) during DisTorch loads
  • Imports FROM Core/Base only

UI Layer: nodes.py, checkpoint_multigpu.py

  • Device-aware user interfaces and node definitions

Assembly: __init__.py

  • Final integration/patch registration (mm.soft_empty_cache patch, node maps)

Import Flow Architecture

    ┌─────────────────┐
    │    __init__.py  │ ← Assembly Layer
    └─────────────────┘
           ↑
    ┌─────────────────┐
    │  UI Layer       │ ← nodes.py, checkpoint_multigpu.py
    └─────────────────┘
           ↑
    ┌─────────────────┐
    │ Feature Layer   │ ← distorch_2.py, distorch.py
    └─────────────────┘
           ↑
    ┌─────────────────┐
    │ Core Layer      │ ← model_management_mgpu.py
    └─────────────────┘
           ↑
    ┌─────────────────┐
    │ Base Layer      │ ← device_utils.py  
    └─────────────────┘

Architectural Validation

Rule: Dependencies only flow UPWARD. Violations create circular imports.

Prevention: Before any import, verify it respects the layer hierarchy.

Function Migration Record

Moved from device_utils.py to model_management_mgpu.py:

  • multigpu_memory_log – memory state logging
  • trigger_executor_cache_reset – CPU memory management
  • check_cpu_memory_threshold – adaptive cleanup triggers
  • force_full_system_cleanup – Manager-parity free flow

Rationale: These belong to model lifecycle/cleanup, not hardware enumeration.