DoRA Learned Extract (Quantile) (WIP)
DoRA Learned Extract (Quantile) — percentage-targeted extraction, gradient-refined
- output_path
Same idea as DoRAExtractQuantile - pick a target percentage of cumulative singular-value energy to keep per layer instead of a fixed rank - with a gradient-descent refinement stage added on top. And like the rest of this sub-family, it ships labeled "(WIP)" in its own display name, which is the pack author being upfront that this is the least-proven corner of the extraction tools rather than a finished feature you should trust without checking.
The refinement idea across all five "Learned" nodes here is the same: SVD gives you the provably optimal low-rank approximation of a weight matrix in isolation, but that's a narrower guarantee than it sounds - it says nothing about how the result behaves once it's reassembled into DoRA's magnitude-and-direction format and actually loaded, and it treats every layer as an independent problem rather than accounting for how they interact. So this node uses the quantile-targeted SVD result as an initialization, then runs an optimizer to directly minimize the reconstruction error against the real weight delta, on the bet that direct loss minimization can close some of that gap. It's a reasonable bet. It's also explicitly unverified, per the label.
Inputs that matter. model_a (finetune) and model_b (base) - the pair being diffed. linear_quantile and conv_quantile (both default 0.9) are your actual target: keep adding singular values, largest first, until you've captured 90% of that layer's cumulative singular-value sum, split separately for linear/attention and convolutional layers since they often need different amounts of rank to hit the same target. linear_max_rank and conv_max_rank (both default 128) cap the rank regardless of what the quantile target would otherwise demand.
The optimization stage: optimize_iters (default 500, set to 0 to skip refinement and fall back to plain quantile-based SVD, which is functionally what DoRAExtractQuantile already gives you for less compute), optimizer (default "prodigy," an adaptive optimizer whose whole selling point is not needing you to tune a learning rate by hand), learning_rate (default 1 - appropriate for Prodigy; the tooltip specifically warns AdamW wants closer to 0.01, so don't leave this at 1 if you switch optimizers), lr_schedule (default "plateau") shaped by lr_patience (default 2), lr_factor (default 0.9), and lr_cooldown (default 2), plus early stopping via early_stop_loss (1e-6), early_stop_stall (2000 iterations without improvement), and early_stop_lr (9.01e-9).
Shared plumbing: chunk_large_layers for fused QKV/MLP blocks, clamp_quantile (default 0.99) to bound outlier singular values, min_diff (default 0) to skip near-untouched layers, mismatch_mode (default "skip"), skip_patterns/glob_skip_patterns for manual layer exclusion, output_filename (default "extracted_lora"), save_dtype (default fp16), device (default cuda), lazy_load (on by default), force_clear_cache (on by default).
Output. output_path (STRING). Output node - it runs as a terminal step in the graph regardless of downstream wiring.
Installing it. ComfyUI Manager, search "Model Utility Toolkit," or:
cd ComfyUI/custom_nodes
git clone https://github.com/silveroxides/ComfyUI-ModelUtils
then restart.
Troubleshooting. Given the WIP status, the most useful check is a direct comparison: run this and the plain DoRAExtractQuantile on the same model pair at the same quantile targets, then actually load both results and see whether the refined version behaves noticeably better - don't assume the extra compute automatically buys you something. If optimize_iters consistently exhausts its full budget without triggering an early stop, that's almost always a learning-rate mismatch with your chosen optimizer rather than evidence the model pair is unusually resistant to fitting. And as with every node in this extraction family, two full checkpoints have to be resident at once for the diff - leave lazy_load and force_clear_cache on unless you know you have the VRAM to spare.
Inputs (27)
| Name | Type | Default | Description |
|---|---|---|---|
| model_a | COMBO | Finetuned model (A - B = LoRA) | |
| model_b | COMBO | Base model (A - B = LoRA) | |
| linear_quantile | FLOAT | 0.900–1 | — |
| conv_quantile | FLOAT | 0.900–1 | — |
| linear_max_rank | INT | 1281–16384 | — |
| conv_max_rank | INT | 1281–16384 | — |
| optimize_iters | INT | 5000–10000 | Number of gradient descent iterations (0 = skip optimization) |
| learning_rate | FLOAT | 1.0000.0001–100 | Base learning rate. AdamW prefers ~0.01, Prodigy prefers ~1.0 |
| optimizer | COMBO | prodigy | Optimization algorithm |
| lr_schedule | COMBO | plateau | Learning rate scheduling strategy |
| lr_patience | INT | 20–1000 | Steps to wait before decaying LR (Plateau) |
| lr_factor | FLOAT | 0.900.01–1 | Factor to decay LR by (Plateau) |
| lr_cooldown | INT | 20–1000 | Steps to wait after decay before checking again (Plateau) |
| early_stop_loss | FLOAT | 00–1 | Stop early if MSE loss drops below this value |
| early_stop_stall | INT | 20000–10000 | Stop early if loss doesn't improve for this many iterations |
| early_stop_lr | FLOAT | 00–1 | Stop early if learning rate drops below this value |
| lazy_load | BOOLEAN | true | Low memory mode: load tensors from disk on demand |
| force_clear_cache | BOOLEAN | true | Clear CUDA cache after each layer |
| chunk_large_layers | BOOLEAN | false | Split large fused layers (QKV, MLP) into chunks |
| clamp_quantile | FLOAT | 0.990.5–1 | Clamp outlier singular values |
| min_diff | FLOAT | 0.0000–1 | Skip layers with max difference below this |
| mismatch_mode | COMBO | skip | 3 options: skip, zeros, error |
| output_filename | STRING | extracted_lora | — |
| save_dtype | COMBO | fp16 | 3 options: fp16, bf16, fp32 |
| device | COMBO | cuda | 2 options: cuda, cpu |
| skip_patterns | STRING | Patterns for layers to skip (regex or glob depending on glob_skip_patterns) | |
| glob_skip_patterns | BOOLEAN | false | When True, skip_patterns use glob syntax (* = any sequence, ? = any char, dots are literal). When False (default), patterns are Python regex matched as substrings. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| output_path | STRING | — |