AttributeError: 'Linear' object has no attribute 'weight_scale' — ModelMergeSimple on a quantized (fp8/QuantizedTensor) model: unguarded getattr in get_key_weight vs synthetic quant state-dict keys
- Dominant language
- Python
- Stars
- 133k
- Forks
- 15.7k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 158
Description
### Custom Node Testing
- [x] I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
Core nodes only — no custom nodes involved in the repro.
### Expected Behavior
`ModelMergeSimple` (and the other `get_key_patches`-based merge nodes) should be able to merge a checkpoint whose weights were loaded as `QuantizedTensor` (e.g. an FP8 e4m3fn mixed-precision quantized checkpoint). The real `.weight` keys already have a working `convert_weight` dequantization path in `get_key_weight`, so quantized merging is clearly intended to work.
### Actual Behavior
Deterministic crash at the merge node, before any sampling, whenever **model2** (the model `get_key_patches` is called on) is a quantized checkpoint:
```
AttributeError: 'Linear' object has no attribute 'weight_scale'
```
Observed on a v0.24.0-based build; I verified the relevant code is unchanged on current master (line numbers below are from master).
### Steps to Reproduce
1. Load an FP8 e4m3fn quantized checkpoint (e.g. a quantized FLUX.2 Klein — any checkpoint that carries `comfy_quant` metadata so its weights load as `QuantizedTensor` via the Mixed Precision Quantization System, #10498 / #11000).
2. Load any second model of the same architecture.
3. Connect both to `ModelMergeSimple` (quantized model as `model2`) and queue.
4. Instant `AttributeError` — 100% reproducible, no GPU/sampling needed.
Affects every node that calls `get_key_patches` on a quantized model: `ModelMergeSimple`, `ModelMergeBlocks`, `ModelMergeSubtract`, `ModelMergeAdd`, `CLIPMergeSimple`, `CheckpointSave`-after-merge, etc.
### Debug Logs
```powershell
Traceback (most recent call last):
File "ComfyUI/comfy_extras/nodes_model_merging.py", line 28, in merge
kp = model2.get_key_patches("diffusion_model.")
File "ComfyUI/comfy/model_patcher.py", line 822, in get_key_patches
weight, set_func, convert_func = get_key_weight(self.model, k)
File "ComfyUI/comfy/model_patcher.py", line 186, in get_key_weight
weight = getattr(op, op_keys[1])
File "torch/nn/modules/module.py", line 1928, in __getattr__
raise AttributeError(
AttributeError: 'Linear' object has no attribute 'weight_scale'
```
(Traceback trimmed to the relevant frames; from a headless server deployment.)
### Other
**Root cause**
The Mixed Precision Quantization System makes quantized layers' `state_dict()` emit *synthetic* top-level keys that are **not** module attributes:
- `comfy/ops.py:1118` `_quantized_weight_state_dict` writes `{prefix}weight_scale` (via `module.weight.state_dict(f"{prefix}weight")`), `{prefix}comfy_quant` (line 1136), `{prefix}input_scale` (`extra_quant_params`, line 1181), and `{prefix}weight_scale_2` for nvfp4.
- On load these keys are popped and absorbed into the `QuantizedTensor` layout params — `_load_quantized_module` explicitly **skips** `register_parameter` for `weight_scale` / `weight_scale_2` (`comfy/ops.py:1103-1104`). So they exist only in the state dict, never on the module.
Meanwhile `ModelPatcher.get_key_patches` (`comfy/model_patcher.py:813`) iterates **all** `model_state_dict()` keys and feeds each one to `get_key_weight`, which does an unguarded lookup (`comfy/model_patcher.py:186`):
```python
weight = getattr(op, op_keys[1])
```
For `some.layer.weight_scale` this resolves the module fine but then hits `torch.nn.Module.__getattr__` → `AttributeError`.
The old fix f9f9fafac ("Fixed model merging issue with scaled fp8", 2024) only covered the legacy `scaled_fp8` buffer format, which *is* a real module attribute — it doesn't help with the new synthetic keys.
**Suggested fix** (running in our fork, resolves the crash; quantized `.weight` keys keep their `convert_weight` dequant path so merging still works as designed):
```python
# comfy/model_patcher.py get_key_weight
weight = getattr(op, op_keys[1], None)
if weight is None:
return None, None, None
```
plus skipping `None` entries in `get_key_patches`:
```python
weight, set_func, convert_func = get_key_weight(self.model, k)
if weight is None:
continue
```
If returning `None` from `get_key_weight` is considered too broad (see the discussion in #11585 about not sweeping bugs under the rug), an alternative is for `get_key_patches` to explicitly skip the known synthetic suffixes (`comfy_quant`, `weight_scale`, `weight_scale_2`, `input_scale`). Unlike #11585 (triggered by a custom node's `RMS_norm.gamma`), here the offending keys are generated by core's own quantized `state_dict()`, so core nodes alone hit it.
**Related but not duplicates**
- #13637 / #13650: Flux.2 Klein merge+save segfault in the safetensors *save* path, fixed by #13835 — that's downstream of this crash; with `ModelMergeSimple` + quantized `model2` you never even reach saving.
- #11585: same unguarded `getattr` line, different trigger (custom-node `RMS_norm` without `.weight`).
Contributor guide
Assessment
This issue has not been assessed yet.