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:
ComfyUI: model.forward(x, t, context=c, control=ctrl, ...)
Nunchaku: def forward(..., control=None, ...):
Python: Receives and processes the control parameter
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:
Text encoder (CLIP)
context or y (either or both)
Combine with temporal information in time_text_embed
Transformer cross-attention
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:
ComfyUI startup passes device_id as None as default value
GPU selection is skipped in multi-GPU environment
During initialization in CPU-only environment
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") → OKComplete 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 NoneCode 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 nComparison 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 savedCode 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 = NoneCode 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 outputPatch 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
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)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)When applying LoRA
Model keys ≈ LoRA keys
→ strict=True is OK
Scenario 2: With PuLID Applied (Error Occurs)
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)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_caModel'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 addedCode 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 executeLoRA 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 independentlyOverall 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 AttributeErrorCode 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)
passDetailed 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 safelyImportance 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 occursHow 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.
