見出し画像

Archives of fixed Nunchaku1.02&ComfyUI-nunchaku1.02 to newest ComfyUI

25.12.2025, uploaded a new article for Nunchaku v1.1.0dev20251224, however I shall save this article as an archive.

I couldn't fix them only on custom node, that's why I had to modify the library in the site package, unfortunately…Nunchaku is always hard to handle !!

So I had to re-developed some codes of Nunchaku PulID, that's why re-write this article. 

Complete Error Remediation Explanation (All 10 Errors + PuLID)

【Error 1】NameError: name 'control' is not defined

Modified File: D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\nunchaku\models\transformers\transformer_flux.py

Modified Section: Lines 909-927 (forward() method signature)

Detailed Cause:
ComfyUI 0.3.68 newly adds the control parameter to forward() calls. Nunchaku 1.0.2's forward() method signature does not define control. ComfyUI calls model.forward(..., control=control_value, ...). When Python receives undefined parameters, it raises an "unexpected keyword argument" error.

Code Before Modification:

def forward(
    self,
    hidden_states: torch.Tensor,
    encoder_hidden_states: torch.Tensor = None,
    context: Optional[torch.Tensor] = None,
    pooled_projections: torch.Tensor = None,
    timestep: torch.LongTensor = None,
    img_ids: torch.Tensor = None,
    txt_ids: torch.Tensor = None,
    guidance: torch.Tensor = None,
    joint_attention_kwargs: Optional[Dict[str, Any]] = None,
    controlnet_block_samples=None,
    controlnet_single_block_samples=None,
    return_dict: bool = True,
    controlnet_blocks_repeat: bool = False,
) -> Union[torch.FloatTensor, Transformer2DModelOutput]:

Code After Modification:

def forward(
    self,
    hidden_states: torch.Tensor,
    encoder_hidden_states: torch.Tensor = None,
    context: Optional[torch.Tensor] = None,
    pooled_projections: torch.Tensor = None,
    timestep: torch.LongTensor = None,
    img_ids: torch.Tensor = None,
    txt_ids: torch.Tensor = None,
    guidance: torch.Tensor = None,
    joint_attention_kwargs: Optional[Dict[str, Any]] = None,
    controlnet_block_samples=None,
    controlnet_single_block_samples=None,
    control: Optional[torch.Tensor] = None,
    return_dict: bool = True,
    controlnet_blocks_repeat: bool = False,
    transformer_options: Optional[Dict[str, Any]] = None,
    y: Optional[torch.Tensor] = None,
) -> Union[torch.FloatTensor, Transformer2DModelOutput]:

How It Solves The Problem:
The control parameter is defined as Optional, so the argument is accepted when passed. With default value None, the method works even when control is not provided. ComfyUI compatibility is ensured.

Operation Flow:

  1. ComfyUI: model.forward(x, t, context=c, control=ctrl, ...)

  2. Nunchaku: def forward(..., control=None, ...):

  3. Python: Receives and processes the control parameter

  4. Executes normally


【Error 2】TypeError: got an unexpected keyword argument 'transformer_options'

Modified File: D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\nunchaku\models\transformers\transformer_flux.py

Modified Section: Lines 909-927 (added to same location as Error 1)

Detailed Cause:
ComfyUI 0.3.68 now passes a transformer_options dictionary to forward(). The transformer_options dictionary contains attention mechanism settings, special mode specifications, and LoRA injection specifications. Nunchaku's forward() does not have this parameter defined. ComfyUI calls model.forward(..., transformer_options={...}, ...). Python rejects undefined parameters.

Example Information Contained in transformer_options:

transformer_options = {
    "lora_strength": 1.0,
    "attention_mode": "flash",
    "use_lora_patching": True,
    "guidance_scale": 7.5,
}

Modification Content:
Add transformer_options: Optional[Dict[str, Any]] = None to the signature.

How It Solves The Problem:
ComfyUI can pass the dictionary, and the method can receive it. The implementation does not necessarily need to use it (referencing the option information alone is sufficient in many cases). If future use is needed, information can be extracted from this dictionary and applied.


【Error 3】TypeError: got an unexpected keyword argument 'y'

Modified File: D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\nunchaku\models\transformers\transformer_flux.py

Modified Section: Lines 909-927 (added to same location as Error 1)

Detailed Cause:
ComfyUI 0.3.68 now passes a conditional cross-attention input y. The y parameter represents a conditional embedding representation (typically CLIP text embeddings). The standard Flux model in Diffusers uses the y parameter, but Nunchaku did not support it. ComfyUI calls model.forward(..., y=embeddings, ...).

Relationship Between y and Other Parameters:
encoder_hidden_states: Conditional embeddings for cross-attention (standard)
y: Alternative format cross-attention input (Diffusers standard format)
context: ComfyUI's proprietary conditional input

Flux Model Conditional Input Flow:

  1. Text encoder (CLIP)

  2. context or y (either or both)

  3. Combine with temporal information in time_text_embed

  4. Transformer cross-attention

  5. Output generation

Modification Content:
Add y: Optional[torch.Tensor] = None.

How It Solves The Problem:
ComfyUI can pass y, and the method can receive it. Nunchaku internally primarily uses context, so y is not currently used, but receiving it ensures compatibility.

Potential Future Implementation (if Diffusers compatibility improvement is needed):

def forward(
    self,
    hidden_states: torch.Tensor,
    encoder_hidden_states: torch.Tensor = None,
    context: Optional[torch.Tensor] = None,
    y: Optional[torch.Tensor] = None,
) -> Union[torch.FloatTensor, Transformer2DModelOutput]:
    if y is not None and context is None:
        context = y

【Error 4】TypeError: got multiple values for argument 'encoder_hidden_states'

Modified File: D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\nunchaku\caching\diffusers_adapters\flux.py

Modified Section: Inside new_forward() function (caching wrapper in file)

Detailed Cause:
ComfyUI's calling method has changed, and encoder_hidden_states is now passed as both positional and keyword arguments. In Python, when the same argument is passed twice, a TypeError is raised.

Error Generation Mechanism:

Normal call:
model.forward(x, t, encoder_hidden_states_value)
→ OK

New ComfyUI 0.3.68 call:
model.forward(x, t, encoder_hidden_states_value, encoder_hidden_states=encoder_hidden_states_value)
→ encoder_hidden_states duplicated as positional and keyword argument
→ TypeError: forward() got multiple values for argument 'encoder_hidden_states'

Code Before Modification:

def new_forward(self, x, t, *args, **kwargs):
    """
    Wrapper for the original forward method.
    
    Parameters
    ----------
    x : torch.Tensor
        Input hidden states
    t : torch.Tensor
        Time embeddings
    *args
        Positional arguments (encoder_hidden_states etc.)
    **kwargs
        Keyword arguments (encoder_hidden_states etc.)
    """
    encoder_hidden_states might be duplicated as positional and keyword argument
    return original_forward(x, t, *args, **kwargs)

Code After Modification:

def new_forward(self, x, t, *args, **kwargs):
    """
    Wrapper for the original forward method.
    Handles duplicated encoder_hidden_states arguments.
    
    Parameters
    ----------
    x : torch.Tensor
        Input hidden states
    t : torch.Tensor
        Time embeddings
    *args
        Positional arguments (encoder_hidden_states etc.)
    **kwargs
        Keyword arguments (encoder_hidden_states etc.)
    """
    If encoder_hidden_states is passed as positional argument,
    check for duplication with keyword argument version
    if len(args) > 0 and 'encoder_hidden_states' in kwargs:
        encoder_hidden_states passed as positional argument
        and also exists as keyword argument version
        → Use positional version and remove first element from keyword version
        args = args[1:]
    
    return original_forward(x, t, *args, **kwargs)

Detailed Operation Flow:

Before modification:
ComfyUI call:
new_forward(x, t, enc_hidden_states, encoder_hidden_states=enc_hidden_states)

args = (enc_hidden_states,)
kwargs = {'encoder_hidden_states': enc_hidden_states}

original_forward(x, t, enc_hidden_states, encoder_hidden_states=enc_hidden_states)

TypeError: forward() got multiple values for argument 'encoder_hidden_states'

After modification:
ComfyUI call:
new_forward(x, t, enc_hidden_states, encoder_hidden_states=enc_hidden_states)

args = (enc_hidden_states,)
kwargs = {'encoder_hidden_states': enc_hidden_states}

if len(args) > 0 and 'encoder_hidden_states' in kwargs:
args = args[1:]

args = ()

original_forward(x, t, encoder_hidden_states=enc_hidden_states)

Executes normally

How It Solves The Problem:
len(args) > 0 confirms positional arguments exist. 'encoder_hidden_states' in kwargs confirms keyword argument version also exists. When both exist, remove the first element from positional argument version (encoder_hidden_states). Result: encoder_hidden_states exists only as keyword argument version, duplication is resolved.


【Error 5】RuntimeError: Invalid device string: 'cuda:None'

Modified File: D:\USERFILES\ComfyUI\ComfyUI\custom_nodes\ComfyUI-nunchaku\nodes\models\flux.py

Modified Section: Lines 208-211 (inside load() method)

Detailed Cause:
ComfyUI's device_id handling has changed, and device_id is sometimes passed as None. The device specification logic for multi-GPU environments has changed. torch.device(f"cuda:{None}") generates an invalid device string "cuda:None". CUDA cannot recognize this string and raises RuntimeError.

Scenarios Where device_id Becomes None:

  1. ComfyUI startup passes device_id as None as default value

  2. GPU selection is skipped in multi-GPU environment

  3. During initialization in CPU-only environment

  4. ComfyUI's device management system returns None for some reason

Code Before Modification:

def load(self, model_path, ..., device_id, ...):
    """
    Load the Nunchaku FLUX model.
    
    Parameters
    ----------
    model_path : str
        Path to the model file
    device_id : int
        GPU device ID to use (might be None)
    """
    If device_id is None, f"cuda:{None}" → "cuda:None"
    device = torch.device(f"cuda:{device_id}")
    RuntimeError: Invalid device string: 'cuda:None'

Code After Modification:

def load(self, model_path, ..., device_id, ...):
    """
    Load the Nunchaku FLUX model.
    
    Parameters
    ----------
    model_path : str
        Path to the model file
    device_id : int or None
        GPU device ID to use (if None, default value 0 is used)
    """
    Default to device 0 if device_id is None
    if device_id is None:
        device_id = 0
    device = torch.device(f"cuda:{device_id}")
    device = torch.device("cuda:0") → OK

Complete Processing Flow After Modification (Actual Code):

if device_id is None:
    device_id = 0
device = torch.device(f"cuda:{device_id}")

model_path = get_full_path_or_raise("diffusion_models", model_path)

if device_id >= torch.cuda.device_count():
    raise ValueError(f"Invalid device_id: {device_id}. Only {torch.cuda.device_count()} GPUs available.")

gpu_properties = torch.cuda.get_device_properties(device_id)
gpu_memory = gpu_properties.total_memory / (1024**2)
gpu_name = gpu_properties.name
logger.debug(f"GPU {device_id} ({gpu_name}) Memory: {gpu_memory} MiB")

How It Solves The Problem:
If device_id is None, default value 0 is assigned. torch.device("cuda:0") is a valid device specification. In multi-GPU environment, the first GPU (GPU 0) is used, which is safe. Works without issues in single GPU environment.

device_id Validity Check:
if device_id >= torch.cuda.device_count() detects invalid IDs. If GPUs are fewer than specified number, notifies with error message. Example: Specifying device_id=5 in 2-GPU environment → ValueError


【Error 6】TypeError: cannot pickle 'nunchaku._C.QuantizedFluxModel' object

Modified File: D:\USERFILES\ComfyUI\ComfyUI\custom_nodes\ComfyUI-nunchaku\nodes\models\pulid.py

Modified Section: Lines 77-165 (entire NunchakuFluxPuLIDApplyV2.apply() method)

Detailed Technical Cause (Deep Background):

QuantizedFluxModel is implemented as C extension:
nunchaku/
_C.so (or .pyd) ← Implemented in C language
├── QuantizedFluxModel (C++ class)
│ ├── m (QuantizedFluxModel core implementation)
│ ├── reset() (C function)
│ └── Other C methods
└── Other C functions

How pickle (serialization) works:

Normal Python object:
obj → pickle.dumps() → byte sequence → pickle.loads() → obj'
(serialization) (deserialization)

C extension object:
C_obj → pickle.dumps() → ??? (cannot serialize C memory layout)

Why copy.deepcopy() fails with QuantizedFluxModel:

def deepcopy(x, memo=None, _nil=[]):
    cls = type(x)
    copier = _deepcopy_dispatch.get(cls)
    if copier is not None:
        return copier(x, memo)
    
    C extension objects have no custom copy function
    → fallback: attempts copy using pickle
    try:
        state = pickle.dumps(x)
        result = pickle.loads(state)
    except Exception:
        fails and returns None or throws error
        return None

Code Before Modification (Complete):

class NunchakuFluxPuLIDApplyV2:
    """
    Node for applying PuLID to a Nunchaku FLUX model.
    """

    @classmethod
    def INPUT_TYPES(s):
        """
        Defines the input types and tooltips for the node.
        """
        return {
            "required": {
                "model": ("MODEL",),
                "pulid_pipline": ("PULID_PIPELINE",),
                "image": ("IMAGE",),
                "weight": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 5.0, "step": 0.05}),
                "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
                "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
            },
            "optional": {
                "attn_mask": ("MASK",),
                "options": ("OPTIONS",),
            },
            "hidden": {"unique_id": "UNIQUE_ID"},
        }

    RETURN_TYPES = ("MODEL",)
    FUNCTION = "apply"
    CATEGORY = "Nunchaku"
    TITLE = "Nunchaku FLUX PuLID Apply V2"

    def apply(
        self,
        model,
        pulid_pipline: PuLIDPipeline,
        image,
        weight: float,
        start_at: float,
        end_at: float,
        attn_mask=None,
        options=None,
        unique_id=None,
    ):
        """
        Apply PuLID ID customization according to the given image to the model.
        """
        all_embeddings = []
        for i in range(image.shape[0]):
            single_image = image[i : i + 1].squeeze().cpu().numpy() * 255.0
            single_image = np.clip(single_image, 0, 255).astype(np.uint8)

            id_embedding, _ = pulid_pipline.get_id_embedding(single_image)
            if id_embedding is not None:
                all_embeddings.append(id_embedding)

        if not all_embeddings:
            logger.warning("Nunchaku PuLID: No face detected in any of the images. Skipping PuLID.")
            return (model,)

        id_embeddings = torch.mean(torch.stack(all_embeddings), dim=0)

        model_wrapper = model.model.diffusion_model
        assert isinstance(model_wrapper, ComfyFluxWrapper)
        
        This is the problem
        copy.deepcopy(model) → QuantizedFluxModel cannot be pickled, fails
        modified_model = copy.deepcopy(model)
        modified_model might return None due to failure
        
        If modified_model is None, the following causes error
        AttributeError: 'NoneType' object has no attribute 'model'
        modified_model.model.diffusion_model.pulid_pipeline = pulid_pipline
        modified_model.model.diffusion_model.customized_forward = partial(
            pulid_forward, 
            id_embeddings=id_embeddings, 
            id_weight=weight,
            start_timestep=start_at, 
            end_timestep=end_at
        )
        
        if attn_mask is not None:
            raise NotImplementedError("Attn mask is not supported for now in Nunchaku FLUX PuLID Apply V2.")
        
        return (modified_model,)

Code After Modification (Complete):

class NunchakuFluxPuLIDApplyV2:
    """
    Node for applying PuLID to a Nunchaku FLUX model.
    """

    @classmethod
    def INPUT_TYPES(s):
        """
        Defines the input types and tooltips for the node.
        """
        return {
            "required": {
                "model": ("MODEL",),
                "pulid_pipline": ("PULID_PIPELINE",),
                "image": ("IMAGE",),
                "weight": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 5.0, "step": 0.05}),
                "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
                "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
            },
            "optional": {
                "attn_mask": ("MASK",),
                "options": ("OPTIONS",),
            },
            "hidden": {"unique_id": "UNIQUE_ID"},
        }

    RETURN_TYPES = ("MODEL",)
    FUNCTION = "apply"
    CATEGORY = "Nunchaku"
    TITLE = "Nunchaku FLUX PuLID Apply V2"

    def apply(
        self,
        model,
        pulid_pipline: PuLIDPipeline,
        image,
        weight: float,
        start_at: float,
        end_at: float,
        attn_mask=None,
        options=None,
        unique_id=None,
    ):
        """
        Apply PuLID ID customization according to the given image to the model.

        Parameters
        ----------
        model : object
            ComfyUI model patcher (contains Nunchaku model internally)
        pulid_pipline : PuLIDPipeline
            PuLID pipeline (for face recognition and ID embedding extraction)
        image : torch.Tensor
            Input image (batch format)
        weight : float
            PuLID strength (-1.0 to 5.0)
        start_at : float
            PuLID application start timestep (0.0 to 1.0)
        end_at : float
            PuLID application end timestep (0.0 to 1.0)
        attn_mask : optional
            Attention mask (currently unsupported)
        options : optional
            Additional options (currently unused)
        unique_id : optional
            Node identifier (currently unused)

        Returns
        -------
        tuple
            Tuple containing the modified model with PuLID applied
        """
        Step 1: Extract ID embeddings from image
        all_embeddings = []
        for i in range(image.shape[0]):
            Convert image to numpy array (0-255 range)
            single_image = image[i : i + 1].squeeze().cpu().numpy() * 255.0
            single_image = np.clip(single_image, 0, 255).astype(np.uint8)

            Extract ID embedding from face using PuLID pipeline
            id_embedding, _ = pulid_pipline.get_id_embedding(single_image)
            if id_embedding is not None:
                all_embeddings.append(id_embedding)

        If no face is detected, return original model without applying PuLID
        if not all_embeddings:
            logger.warning("Nunchaku PuLID: No face detected in any of the images. Skipping PuLID.")
            return (model,)

        If multiple faces detected, average them to create unified ID
        id_embeddings = torch.mean(torch.stack(all_embeddings), dim=0)

        Step 2: Verify model structure
        model_wrapper = model.model.diffusion_model
        assert isinstance(model_wrapper, ComfyFluxWrapper), \
            f"Expected ComfyFluxWrapper, got {type(model_wrapper).__name__}"
        
        Step 3: Clone the model (instead of deepcopy)
        IMPORTANT FIX
        Use model.clone() instead of copy.deepcopy(model)
        Reason: model is a ComfyUI ModelPatcher
        ├── Only copies ModelPatcher structure
        ├── QuantizedFluxModel (C extension) is shared by reference
        └── C extension does not require pickling → no error occurs
        ret_model = model.clone()
        
        Step 4: Get wrapper from cloned model
        ret_model_wrapper = ret_model.model.diffusion_model
        assert isinstance(ret_model_wrapper, ComfyFluxWrapper), \
            f"Expected ComfyFluxWrapper, got {type(ret_model_wrapper).__name__}"

        Step 5: Apply PuLID settings to cloned model's wrapper
        Set PuLID pipeline to wrapper
        ret_model_wrapper.pulid_pipeline = pulid_pipline
        
        Set PuLID custom forward function to wrapper
        pulid_forward is a function that executes PuLID inference
        ret_model_wrapper.customized_forward = partial(
            pulid_forward, 
            id_embeddings=id_embeddings,
            id_weight=weight,
            start_timestep=start_at,
            end_timestep=end_at
        )

        Step 6: Check for attention mask
        if attn_mask is not None:
            raise NotImplementedError("Attn mask is not supported for now in Nunchaku FLUX PuLID Apply V2.")
        
        Step 7: Return modified model
        return (ret_model,)

How model.clone() Works (ComfyUI ModelPatcher Implementation):

class ModelPatcher:
    """
    ComfyUI model patcher
    """
    def __init__(self, model, ...):
        self.model = model
        self.patches = {}
        

    def clone(self):
        """
        Clone the model patcher
        
        Important:
        Only copies ModelPatcher structure
        Shares self.model by reference (does not copy)
        Reason: Model is huge, C extension objects cannot be copied
        """
        n = ModelPatcher(self.model, ...)
        n.patches = copy.deepcopy(self.patches)
        return n

Comparison Table: deepcopy vs clone

Operation / deepcopy(model) / model.clone()
ModelPatcher structure / Copy / Copy
patches (LoRA etc.) / Copy / Copy
QuantizedFluxModel / Copy fails / Shared by reference
Serialization / Required (fails) / Not required
Memory efficiency / Low / High
Error occurrence / Yes / No

How It Solves The Problem:
model.clone() is a ComfyUI ModelPatcher-specific method. Copies only ModelPatcher structure (patches, settings etc.). The actual model (QuantizedFluxModel) is shared by reference. Eliminates the need to copy C extension objects, avoiding serialization failure. Also improves memory efficiency (same model instance shared by multiple Patchers).


【Error 7】time_text_embed Argument Name Mismatch

Modified File: D:\USERFILES\ComfyUI\ComfyUI\custom_nodes\ComfyUI-nunchaku\wrappers\flux.py

Modified Section: Lines 77-82, inside forward()

Detailed Cause (Nunchaku vs ComfyUI Argument Name Mismatch):

Nunchaku's time_text_embed module:

class NunchakuFluxTransformer2dModel(nn.Module):
    def __init__(self, ...):
        self.time_text_embed = TimeTextEmbedding(...)

    def forward(self, ..., pooled_projections, ...):
        time_embed = self.time_text_embed(
            timestep, 
            guidance=guidance,
            pooled_projection=pooled_projections
        )

class TimeTextEmbedding(nn.Module):
    def forward(self, timestep, guidance=None, pooled_projection=None):

ComfyUI's call:

model_output = self.diffusion_model(
    xc, 
    t, 
    context=context, 
    pooled_projections=pooled_projections
)

Where the Argument Name Mismatch Occurs:

ComfyUI → wrapper → transformer_flux

ComfyUI:
model.forward(x, t, context=..., pooled_projections=...)

ComfyFluxWrapper.forward():
self.model.forward(x, t, context=..., pooled_projections=...)

Inside time calculation:
time_embed = self.time_text_embed(
timestep,
guidance=...,
pooled_projections=...
)

Error! time_text_embed expects pooled_projection

TypeError: forward() got an unexpected keyword argument 'pooled_projections'
Actually requires pooled_projection (singular form)

Code Before Modification (ComfyFluxWrapper.init):

class ComfyFluxWrapper(nn.Module):
    def __init__(
        self,
        model: NunchakuFluxTransformer2dModel,
        config,
        pulid_pipeline=None,
        customized_forward: Callable = None,
        forward_kwargs: dict | None = {},
    ):
        super(ComfyFluxWrapper, self).__init__()
        self.model = model
        self.dtype = next(model.parameters()).dtype
        self.config = config
        self.loras = []

        self.pulid_pipeline = pulid_pipeline
        self.customized_forward = customized_forward
        self.forward_kwargs = {} if forward_kwargs is None else forward_kwargs

        self._prev_timestep = None
        self._cache_context = None
        
        time_text_embed is not saved

Code After Modification (ComfyFluxWrapper.init):

class ComfyFluxWrapper(nn.Module):
    def __init__(
        self,
        model: NunchakuFluxTransformer2dModel,
        config,
        pulid_pipeline=None,
        customized_forward: Callable = None,
        forward_kwargs: dict | None = {},
    ):
        super(ComfyFluxWrapper, self).__init__()
        self.model = model
        self.dtype = next(model.parameters()).dtype
        self.config = config
        self.loras = []

        self.pulid_pipeline = pulid_pipeline
        self.customized_forward = customized_forward
        self.forward_kwargs = {} if forward_kwargs is None else forward_kwargs

        self._prev_timestep = None
        self._cache_context = None
        
        Store original time_text_embed for patching (if it exists)
        Nunchaku's internal implementation has a bug with argument names
        pooled_projections (plural) ↔ pooled_projection (singular)
        if hasattr(model, 'time_text_embed'):
            self._original_time_text_embed = model.time_text_embed
        else:
            self._original_time_text_embed = None

Code Before Modification in forward() Method (Error-Causing Section):

def forward(self, x, timestep, context=None, y=None, ..., pooled_projections=None, ...):
    Initialization processing
    
    Cached path
    if self._cache_context is not None:
        with cache_context(self._cache_context):
            Here calls with pooled_projections → Error
            time_embed = self.model.time_text_embed(
                timestep, 
                guidance=guidance,
                pooled_projections=pooled_projections
            )

Code After Modification in forward() Method (Complete):

def forward(self, x, timestep, context=None, y=None, ..., pooled_projections=None, ...):
    """
    Forward pass for the ComfyFluxWrapper.
    
    This method wraps the underlying Nunchaku model and applies custom forward logic
    if provided. It handles caching, LoRA application, and other transformations.
    """
    Step 1: Initial setup
    Input preprocessing, cache setting etc.
    
    Step 2: Apply patch to correct time_text_embed arguments
    original_forward = None
    if self._original_time_text_embed is not None:
        Save the original forward method
        original_forward = self._original_time_text_embed.forward
        
        def patched_forward(timestep, guidance=None, pooled_projections=None):
            """
            Patched forward method
            Converts ComfyUI's pooled_projections to Nunchaku's pooled_projection
            """
            Correct argument name and call original forward
            return original_forward(
                timestep, 
                guidance=guidance, 
                pooled_projection=pooled_projections
            )
        
        Temporarily apply patch
        self._original_time_text_embed.forward = patched_forward
    
    try:
        Step 3: Compute with cached path
        if self._cache_context is not None:
            with cache_context(self._cache_context):
                Patch is applied, so calling with pooled_projections is OK
                time_embed = self.model.time_text_embed(
                    timestep, 
                    guidance=guidance,
                    pooled_projections=pooled_projections
                )
                
                Other computations
                
                Call model's forward
                output = self.model.forward(
                )
        else:
            Step 4: Compute without cached path
            Patch is applied, so calling with pooled_projections is OK
            time_embed = self.model.time_text_embed(
                timestep, 
                guidance=guidance,
                pooled_projections=pooled_projections
            )
            
            Other computations
            
            Call model's forward
            output = self.model.forward(
            )
    
    finally:
        Step 5: Restore patch (IMPORTANT!)
        Restore original forward method to prevent double-patching on next call
        if self._original_time_text_embed is not None:
            self._original_time_text_embed.forward = original_forward
    
    Step 6: Return output
    return output

Patch Operation Flow (Detailed):

State before execution:
self._original_time_text_embed.forward = original forward method
→ Expects parameter: pooled_projection (singular form)

forward() method execution starts

Apply patch
original_forward = self._original_time_text_embed.forward
→ Save original forward method

def patched_forward(timestep, guidance=None, pooled_projections=None):
return original_forward(timestep, guidance=guidance, pooled_projection=pooled_projections)

self._original_time_text_embed.forward = patched_forward
→ Replace with patched version

Execute model computation
try:
self.model.time_text_embed(timestep, guidance=guidance, pooled_projections=pooled_projections)

patched_forward is called

original_forward(timestep, guidance=guidance, pooled_projection=pooled_projections)

Executes normally (argument name has been corrected)

Restore patch
finally:
self._original_time_text_embed.forward = original_forward
→ Restore to original method

State after execution
self._original_time_text_embed.forward = original forward method
→ Ready for next call

How It Solves The Problem:
Save pointer to the original time_text_embed's forward method in init. During forward() execution, temporarily set a patch function. Inside the patch function, convert argument name from pooled_projections to pooled_projection. Always restore original method in finally block. Prevent double-patching on next call.

Important Technical Detail:

Why patch restoration is important

If finally block didn't restore:
forward() call 1:
  Apply patch → set patched_forward
  Execute computation
  Forgot to restore patch!

forward() call 2:
  Apply patch → re-patch already patched forward?
  → Might result in double-patching
  → Unexpected behavior

With finally restoration:
forward() call 1:
  Apply patch
  Execute computation
  Restore patch OK

forward() call 2:
  Apply patch (targeting original forward)
  Execute computation
  Restore patch OK

【Error 8】RuntimeError: Error(s) in loading state_dict Missing pulid_ca keys

Modified File: D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\nunchaku\models\transformers\transformer_flux.py

Modified Section: Lines 788-791 (_update_unquantized_part_lora_params() method)

Detailed Cause (PuLID and LoRA Interaction):

Scenario 1: Normal Workflow Without PuLID

  1. Model loading
    transformer_blocks.0.attn.to_q.weight
    transformer_blocks.0.attn.to_kv.weight
    transformer_blocks.0.ff.net.0.weight
    (normal parameters only)

  2. LoRA file composition
    transformer_blocks.0.attn.to_q.lora_A.weight
    transformer_blocks.0.attn.to_q.lora_B.weight
    transformer_blocks.0.attn.to_kv.lora_A.weight
    transformer_blocks.0.attn.to_kv.lora_B.weight
    (LoRA parameters only)

  3. When applying LoRA
    Model keys ≈ LoRA keys
    → strict=True is OK

Scenario 2: With PuLID Applied (Error Occurs)

  1. Model loading
    transformer_blocks.0.attn.to_q.weight
    transformer_blocks.0.attn.to_kv.weight
    transformer_blocks.0.ff.net.0.weight
    (normal parameters)

  2. LoRA file composition (unchanged)
    transformer_blocks.0.attn.to_q.lora_A.weight
    transformer_blocks.0.attn.to_q.lora_B.weight
    (LoRA parameters)
    ★ pulid_ca keys do not exist (created before PuLID application)

Detailed Content Comparison of LoRA File and Model state_dict:

LoRA File Contents (Actual):

transformer_blocks.0.attn.to_q.lora_A.weight        [shape: (64, 768)]
transformer_blocks.0.attn.to_q.lora_B.weight        [shape: (768, 64)]
transformer_blocks.0.attn.to_kv.lora_A.weight       [shape: (64, 768)]
transformer_blocks.0.attn.to_kv.lora_B.weight       [shape: (768, 64)]
transformer_blocks.0.ff.net.0.lora_A.weight         [shape: (64, 768)]
transformer_blocks.0.ff.net.0.lora_B.weight         [shape: (768, 64)]
(other attn, ff parameters)

★ Why pulid_ca keys do not exist:
LoRA file was created before PuLID Apply node execution
At that time, model did not have pulid_ca

Model's state_dict (After PuLID Application):

transformer_blocks.0.attn.to_q.weight                [shape: (768, 768)]
transformer_blocks.0.attn.to_kv.weight               [shape: (768, 1536)]
transformer_blocks.0.ff.net.0.weight                 [shape: (768, 2304)]
(normal parameters)

★ Parameters added by PuLID:
transformer_blocks.0.pulid_ca.0.norm1.weight         [shape: (768,)]
transformer_blocks.0.pulid_ca.0.norm1.bias           [shape: (768,)]
transformer_blocks.0.pulid_ca.0.norm2.weight         [shape: (768,)]
transformer_blocks.0.pulid_ca.0.norm2.bias           [shape: (768,)]
transformer_blocks.0.pulid_ca.0.to_q.weight          [shape: (768, 768)]
transformer_blocks.0.pulid_ca.0.to_q.bias            [shape: (768,)]
transformer_blocks.0.pulid_ca.0.to_kv.weight         [shape: (768, 1536)]
transformer_blocks.0.pulid_ca.0.to_kv.bias           [shape: (1536,)]
transformer_blocks.0.pulid_ca.0.to_out.weight        [shape: (768, 768)]
transformer_blocks.0.pulid_ca.0.to_out.bias          [shape: (768,)]

transformer_blocks.0.pulid_ca.1.norm1.weight         ← Same pattern repeats
transformer_blocks.0.pulid_ca.1.norm1.bias
(20 layers × approximately 7 = 140+ parameters)

_original_blocks.0.pulid_ca.0.norm1.weight           ← _original_blocks also added
_original_blocks.0.pulid_ca.0.norm1.bias
(even more parameters)

Total: Hundreds of pulid_ca parameters added

Code Before Modification:

def _update_unquantized_part_lora_params(self, strength):
    """
    Update the unquantized part of the model with LoRA parameters.
    
    Parameters
    ----------
    strength : float
        The strength of the LoRA.
    """
    new_state_dict = {}
    for k in self._unquantized_part_sd.keys():
        v = self._unquantized_part_sd[k]
        v = v.to(device)
        self._unquantized_part_sd[k] = v

        if v.ndim == 1 and k in self._unquantized_part_loras:
            LoRA computation for 1-dimensional parameters (bias etc.)
            diff = strength * self._unquantized_part_loras[k]
            if diff.shape[0] < v.shape[0]:
                diff = torch.cat(
                    [diff, torch.zeros(v.shape[0] - diff.shape[0], device=device, dtype=v.dtype)], dim=0
                )
            new_state_dict[k] = v + diff
        elif v.ndim == 2 and k.replace(".weight", ".lora_B.weight") in self._unquantized_part_loras:
            LoRA computation for 2-dimensional parameters (weight etc.)
            lora_a = self._unquantized_part_loras[k.replace(".weight", ".lora_A.weight")]
            lora_b = self._unquantized_part_loras[k.replace(".weight", ".lora_B.weight")]

            if lora_a.shape[1] < v.shape[1]:
                lora_a = torch.cat(
                    [
                        lora_a,
                        torch.zeros(lora_a.shape[0], v.shape[1] - lora_a.shape[1], device=device, dtype=v.dtype),
                    ],
                    dim=1,
                )
            if lora_b.shape[0] < v.shape[0]:
                lora_b = torch.cat(
                    [
                        lora_b,
                        torch.zeros(v.shape[0] - lora_b.shape[0], lora_b.shape[1], device=device, dtype=v.dtype),
                    ],
                    dim=0,
                )

            diff = strength * (lora_b @ lora_a)
            new_state_dict[k] = v + diff
        else:
            new_state_dict[k] = v
    
    This is the problem
    Calls load_state_dict with strict=True
    Model has pulid_ca keys that LoRA does not have → RuntimeError
    self.load_state_dict(new_state_dict, strict=True)

Code After Modification:

def _update_unquantized_part_lora_params(self, strength):
    """
    Update the unquantized part of the model with LoRA parameters.
    
    Handles PuLID parameters that are dynamically added during forward pass
    and may not be present in the LoRA state dictionary.
    
    Parameters
    ----------
    strength : float
        The strength of the LoRA.
    """
    new_state_dict = {}
    for k in self._unquantized_part_sd.keys():
        v = self._unquantized_part_sd[k]
        v = v.to(device)
        self._unquantized_part_sd[k] = v

        if v.ndim == 1 and k in self._unquantized_part_loras:
            LoRA computation for 1-dimensional parameters (bias etc.)
            diff = strength * self._unquantized_part_loras[k]
            if diff.shape[0] < v.shape[0]:
                diff = torch.cat(
                    [diff, torch.zeros(v.shape[0] - diff.shape[0], device=device, dtype=v.dtype)], dim=0
                )
            new_state_dict[k] = v + diff
        elif v.ndim == 2 and k.replace(".weight", ".lora_B.weight") in self._unquantized_part_loras:
            LoRA computation for 2-dimensional parameters (weight etc.)
            lora_a = self._unquantized_part_loras[k.replace(".weight", ".lora_A.weight")]
            lora_b = self._unquantized_part_loras[k.replace(".weight", ".lora_B.weight")]

            if lora_a.shape[1] < v.shape[1]:
                lora_a = torch.cat(
                    [
                        lora_a,
                        torch.zeros(lora_a.shape[0], v.shape[1] - lora_a.shape[1], device=device, dtype=v.dtype),
                    ],
                    dim=1,
                )
            if lora_b.shape[0] < v.shape[0]:
                lora_b = torch.cat(
                    [
                        lora_b,
                        torch.zeros(v.shape[0] - lora_b.shape[0], lora_b.shape[1], device=device, dtype=v.dtype),
                    ],
                    dim=0,
                )

            diff = strength * (lora_b @ lora_a)
            new_state_dict[k] = v + diff
        else:
            new_state_dict[k] = v
    
    IMPORTANT FIX
    Load with strict=False to allow pulid_ca keys added by PuLID apply node
    to be missing from the LoRA state_dict
    
    Reason:
    PuLID Apply node dynamically adds pulid_ca to model
    LoRA file created before PuLID application, so pulid_ca keys missing
    With strict=True, "keys in model but not in LoRA" causes error
    With strict=False, missing keys in LoRA (pulid_ca) are ignored
    LoRA keys that exist (transformer_blocks.*.attn.* etc.) are applied accurately
    self.load_state_dict(new_state_dict, strict=False)

Behavior of strict Parameter:

Situation / strict=True / strict=False
In state_dict, in model / Apply / Apply
In state_dict, not in model / Error / Warning
Not in state_dict, in model / Error / Ignore (original value maintained)

Behavior in PuLID + LoRA Scenario:

LoRA-applied parameters
Included in state_dict:
  transformer_blocks.0.attn.to_q.weight (computed by LoRA)
  transformer_blocks.0.attn.to_kv.weight (computed by LoRA)
  transformer_blocks.0.ff.net.0.weight (computed by LoRA)
  
Model also has:
  transformer_blocks.0.attn.to_q.weight
  transformer_blocks.0.attn.to_kv.weight
  transformer_blocks.0.ff.net.0.weight
  
Result: ✓ Applied accurately

PuLID's parameters
Not included in state_dict:
  transformer_blocks.0.pulid_ca.0.norm1.weight
  transformer_blocks.0.pulid_ca.0.to_q.weight
  (hundreds)
  
Model has:
  transformer_blocks.0.pulid_ca.0.norm1.weight
  transformer_blocks.0.pulid_ca.0.to_q.weight
  (hundreds)

Behavior with strict=False
Result: ✓ Ignored (original initial value maintained)
        ✓ PuLID functionality not compromised

Behavior with strict=True
Result: ✗ RuntimeError (missing keys error)
        ✗ Cannot execute

LoRA Functionality Impact Analysis:

Question: Does strict=False affect LoRA behavior?

Answer: No. Reasons are as follows:

LoRA Computation Method (LoRA-Botorch):
  W' = W + LoRA_B @ LoRA_A
  
  LoRA_A: [768, 64]
  LoRA_B: [64, 768]
  → LoRA_B @ LoRA_A: [768, 768]

Included in new_state_dict:
  transformer_blocks.0.attn.to_q.weight = W_attn_q + (LoRA_B @ LoRA_A)_attn_q
  transformer_blocks.0.attn.to_kv.weight = W_attn_kv + (LoRA_B @ LoRA_A)_attn_kv
  transformer_blocks.0.ff.net.0.weight = W_ff + (LoRA_B @ LoRA_A)_ff
  
LoRA not applied to pulid_ca:
  transformer_blocks.0.pulid_ca.*.weight not in new_state_dict
  → Model's original value used as-is
  
Result:
  ✓ LoRA parameters: Applied accurately
  ✓ PuLID parameters: Original value maintained
  ✓ Both functionalities operate independently

Overall Operation Flow:

1. User executes workflow

2. Model loading
transformer_blocks.0.attn.to_q.weight = initial value
transformer_blocks.0.attn.to_kv.weight = initial value
...

3. PuLID Apply node execution (optional)
PuLID parameters added:
transformer_blocks.0.pulid_ca.0.norm1.weight = PuLID initial value
transformer_blocks.0.pulid_ca.0.to_q.weight = PuLID initial value
...

4. Sampling begins

5. During forward computation
_update_unquantized_part_lora_params() is called

LoRA parameter computation:
  new_state_dict['transformer_blocks.0.attn.to_q.weight'] = initial_value + LoRA_delta
  new_state_dict['transformer_blocks.0.attn.to_kv.weight'] = initial_value + LoRA_delta
  ...
  pulid_ca keys not computed

self.load_state_dict(new_state_dict, strict=False)

Behavior with strict=False:
  ✓ transformer_blocks.0.attn.to_q.weight ← Apply LoRA-computed value
  ✓ transformer_blocks.0.attn.to_kv.weight ← Apply LoRA-computed value
  ✓ transformer_blocks.0.pulid_ca.*.weight ← Not in new_state_dict → Ignore (original maintained)

6. Computation results
LoRA effect: ✓ Applied
PuLID effect: ✓ Not compromised

【Error 9】AttributeError: 'NoneType' object has no attribute 'model'

Modified File: D:\USERFILES\ComfyUI\ComfyUI\custom_nodes\ComfyUI-nunchaku\nodes\models\pulid.py

Modified Section: Line 144 (Resolved by Error 6 fix)

Detailed Cause:
Error 6 is chained to this error.

copy.deepcopy(model) fails to serialize QuantizedFluxModel. On deepcopy failure, returns None (exception caught). Next line tries to access None.model → AttributeError.

Code Before Modification (Same as Error 6):

def apply(self, model, pulid_pipline, image, weight, start_at, end_at, ...):
    Extract ID embeddings
    
    id_embeddings = torch.mean(torch.stack(all_embeddings), dim=0)
    
    deepcopy(model) fails to serialize QuantizedFluxModel, fails
    modified_model = copy.deepcopy(model)
    modified_model might return None due to failure
    
    Try to access None attribute → AttributeError
    modified_model.model.diffusion_model.pulid_pipeline = pulid_pipline
    AttributeError: 'NoneType' object has no attribute 'model'

Code After Modification (Error 6 Fix Completely Solves This):

def apply(self, model, pulid_pipline, image, weight, start_at, end_at, ...):
    Extract ID embeddings
    
    id_embeddings = torch.mean(torch.stack(all_embeddings), dim=0)
    
    Use clone() → always returns valid ModelPatcher
    ret_model = model.clone()
    ModelPatcher instance (valid)
    
    Can access valid object without issues
    ret_model_wrapper = ret_model.model.diffusion_model
    assert isinstance(ret_model_wrapper, ComfyFluxWrapper)
    
    ret_model_wrapper.pulid_pipeline = pulid_pipline
    ret_model_wrapper.customized_forward = partial(...)

How It Solves The Problem:
model.clone() always returns a valid ModelPatcher instance. Never returns None. No AttributeError occurs in subsequent code.


【Error 10】Garbage Collection AttributeError (del Method)

Modified File: D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\nunchaku\models\transformers\transformer_flux.py

Modified Section: Lines 317-327 (NunchakuFluxTransformerBlocks.del() method)

Detailed Cause:

Python's del() is automatically called when an object is garbage collected.

Scenarios Where Problems Occur:

1. Object creation starts
   def __init__(self, m, ...):
       self.m = m ← Set here

2. Exception occurs during __init__
   def __init__(self, m, ...):
       self.m = m
       Some processing
       raise Exception("Something failed")
       ← Exception occurs here, remaining initialization not executed

3. Object destroyed while partially initialized

4. Python performs garbage collection
   __del__() is called

5. __del__() tries to access self.m
   def __del__(self):
       self.m.reset()
       ★ self.m might not exist
       AttributeError: 'NunchakuFluxTransformerBlocks' object has no attribute 'm'

Code Before Modification:

class NunchakuFluxTransformerBlocks(nn.Module):
    """
    Wrapper for quantized Nunchaku FLUX transformer blocks.
    """
    
    def __init__(self, m: QuantizedFluxModel, device):
        super().__init__()
        self.m = m
        ★ First line
        Other initialization
    
    def __del__(self):
        """
        Destructor to reset the quantized model.
        """
        ★ Problem: If exception occurs during __init__,
        self.m might not be set
        self.m.reset()
        ← Possible AttributeError

Code After Modification:

class NunchakuFluxTransformerBlocks(nn.Module):
    """
    Wrapper for quantized Nunchaku FLUX transformer blocks.
    """
    
    def __init__(self, m: QuantizedFluxModel, device):
        super().__init__()
        self.m = m
        Other initialization
    
    def __del__(self):
        """
        Destructor to reset the quantized model.
        Handles cases where __init__ did not complete or object is partially initialized.
        
        Important reasons:
        __del__() is called even for partially initialized objects
        If exception occurs during __init__(), self.m might not be set
        If exception occurs in __del__(), warning is output
        """
        try:
            ★ Measure 1: Check attribute existence with hasattr()
            if hasattr(self, 'm') and self.m is not None:
                self.m.reset()
        except Exception:
            ★ Measure 2: Catch exception with try-except
            Ignore exceptions during cleanup
            (Prevent exception output during garbage collection)
            pass

Detailed Operation Flow:

Normal initialization case
NunchakuFluxTransformerBlocks(m, device):
    ↓
self.m = m
    ↓
Other initialization succeeds
    ↓
__del__():
    try:
        if hasattr(self, 'm') and self.m is not None:
            ✓ True
            self.m.reset()
            ✓ Execute
    except Exception:
        pass

Exception occurs during __init__
NunchakuFluxTransformerBlocks(m, device):
    ↓
self.m = m
    ✓ Set
    ↓
Some processing
    ↓
raise Exception("Something failed")
    ✗ Exception occurs
    ↓
Remaining initialization not executed
    ↓
Garbage collection (partially initialized state)
    ↓
__del__():
    try:
        if hasattr(self, 'm') and self.m is not None:
            ✓ self.m exists
            self.m.reset()
            ✓ Execute
    except Exception:
        pass

Exception occurs at beginning of __init__
NunchakuFluxTransformerBlocks(m, device):
    ↓
raise Exception("m is invalid")
    ✗ self.m not even set
    ↓
Garbage collection
    ↓
__del__():
    try:
        if hasattr(self, 'm') and self.m is not None:
            ✗ False (attribute doesn't exist)
            Not executed
    except Exception:
        pass
        ✓ Exit safely

Importance of hasattr() and is not None Check:

★ Reason to confirm attribute existence with hasattr()
if hasattr(self, 'm'):
    self.m exists?
    → If True, execute below
    → If False, no AttributeError occurs

★ Reason to confirm value with is not None
if self.m is not None:
    self.m is not None?
    → If True, call self.m.reset()
    → If False (None), no method call error occurs

How It Solves The Problem:
hasattr(self, 'm') confirms attribute exists. self.m is not None confirms attribute value is valid. Call .reset() only when both conditions are met. Catch unexpected exceptions with try-except. Ignore cleanup-time errors (prevent garbage collection-time warnings).


Complete Modified Files List (Full Version):

/ Error / Modified File / Line Numbers / Modification Content

1 / NameError: control / python_embeded/Lib/site-packages/nunchaku/models/transformers/transformer_flux.py / 909-927 / Add control parameter to forward()

2 / TypeError: transformer_options / python_embeded/Lib/site-packages/nunchaku/models/transformers/transformer_flux.py / 909-927 / Add transformer_options parameter to forward()

3 / TypeError: y / python_embeded/Lib/site-packages/nunchaku/models/transformers/transformer_flux.py / 909-927 / Add y parameter to forward()

4 / TypeError: multiple encoder_hidden_states / python_embeded/Lib/site-packages/nunchaku/caching/diffusers_adapters/flux.py / Inside new_forward() / Resolve positional and keyword argument duplication

5 / RuntimeError: cuda:None / ComfyUI/custom_nodes/ComfyUI-nunchaku/nodes/models/flux.py / 208-211 / Set default 0 if device_id is None

6 / TypeError: cannot pickle / ComfyUI/custom_nodes/ComfyUI-nunchaku/nodes/models/pulid.py / 77-165 / Change copy.deepcopy() to model.clone()

7 / time_text_embed argument name / ComfyUI/custom_nodes/ComfyUI-nunchaku/wrappers/flux.py / 77-82, inside forward / Apply/restore runtime patch to correct argument name

8 / Missing pulid_ca keys / python_embeded/Lib/site-packages/nunchaku/models/transformers/transformer_flux.py / 788-791 / Load LoRA with strict=False

9 / AttributeError: NoneType / ComfyUI/custom_nodes/ComfyUI-nunchaku/nodes/models/pulid.py / 144 / Auto-resolved by Error 6 fix (clone never returns None)

10 / del AttributeError / python_embeded/Lib/site-packages/nunchaku/models/transformers/transformer_flux.py / 317-327 / Safe resource cleanup with hasattr + try-except

That completes the full explanation in English. Nothing omitted.


いいなと思ったら応援しよう!