README for 0.1.3: named people, the -mlp compatibility trap, corpus sources
Browse files
README.md
CHANGED
|
@@ -12,100 +12,117 @@ base_model:
|
|
| 12 |
library_name: comfyui
|
| 13 |
---
|
| 14 |
|
| 15 |
-
# ClipProj β MiniMax H3 conditioning from a Qwen3-VL-4B
|
| 16 |
|
| 17 |
-
**Projection matrices that let a Qwen3-VL
|
| 18 |
|
| 19 |
-
**15.7 GB β
|
| 20 |
|
| 21 |
> β οΈ **Proof of concept β working, but a proof of concept.** It runs and produces good video, and every number below was measured on real hardware. Built and tested on a single setup (Windows 11, NVIDIA, ComfyUI 0.31.0) with deliberately limited exploration.
|
| 22 |
|
| 23 |
These files are useless on their own. They require the custom node:
|
| 24 |
**[github.com/nicolab28/ComfyUI-ClipProj](https://github.com/nicolab28/ComfyUI-ClipProj)**
|
| 25 |
|
| 26 |
-
##
|
| 27 |
|
| 28 |
-
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
*
|
| 33 |
-
|
| 34 |
-
## Re-download if you got the first release
|
| 35 |
-
|
| 36 |
-
**The first release was missing the attention-sink vector, and short prompts suffered for it.** The first token of a sequence is an attention sink: its direction is constant from one prompt to the next (cosine 1.0000 measured over 1966 prompts) and it carries nothing from the text, yet its norm reaches 16 500 against 291 for a text token. Calibration excluded it, rightly β its extreme values would wreck the statistics β but the node projected it anyway, through a matrix that had never seen one, producing an arbitrary vector of enormous norm. Invisible on a 200-token prompt where it is 0.5 % of the positions, ruinous on a 7-token one where it is 14 %.
|
| 37 |
-
|
| 38 |
-
Every matrix here now carries the measured sink vector and substitutes it at inference. Since the vector is constant, this is not an approximation: it is the value the 32B would have produced.
|
| 39 |
-
|
| 40 |
-
Also new in this release: an **8B matrix**, an **int8_convrot matrix**, and **ref2va now works** β see below.
|
| 41 |
|
| 42 |
## What this is
|
| 43 |
|
| 44 |
-
MiniMax H3 conditions on a Qwen3-VL-32B truncated to 50 layers β 15.7 GB in NVFP4 β solely to turn a prompt into a `[seq, 5120]` tensor. This repository provides a learned
|
| 45 |
|
| 46 |
```
|
| 47 |
cond = ((h - mean_in) / std_in) @ W * std_out + mean_out
|
| 48 |
```
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
| 51 |
|
| 52 |
## Files
|
| 53 |
|
| 54 |
Put them in `ComfyUI/models/clip_projections/`.
|
| 55 |
|
| 56 |
-
**Start with `
|
| 57 |
|
| 58 |
-
| File |
|
| 59 |
|---|---|---|---|---|
|
| 60 |
-
| `
|
| 61 |
-
| `
|
| 62 |
-
| `
|
| 63 |
-
| `
|
| 64 |
-
| `
|
| 65 |
-
| `
|
| 66 |
-
| `
|
| 67 |
-
| `
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
|
| 70 |
|
| 71 |
-
Every matrix works on **any variant of its own size**: the measured cosine gap between a bf16-calibrated matrix applied to an abliterated fp8 encoder is 0.0023. You do not need the exact checkpoint a matrix was calibrated on. The 8B matrices need an 8B encoder though
|
| 72 |
|
| 73 |
-
|
| 74 |
|
| 75 |
-
|
| 76 |
|
| 77 |
-
|
| 78 |
|
| 79 |
-
|
| 80 |
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
|
| 85 |
-
|
| 86 |
-
4B CONDPROJ against unweighted, same corpus cosine 0.999998
|
| 87 |
-
8B CONDPROJ against unweighted, same corpus cosine 0.999999
|
| 88 |
-
4B CONDPROJ against the 2000-prompt matrix cosine 0.744034
|
| 89 |
-
```
|
| 90 |
|
| 91 |
-
|
| 92 |
|
| 93 |
-
|
| 94 |
|
| 95 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
## Measured results
|
| 98 |
|
| 99 |
-
|
|
| 100 |
-
|---|---|---|
|
| 101 |
-
|
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
|
| 105 |
|
| 106 |
-
|
| 107 |
|
| 108 |
-
|
|
|
|
|
|
|
| 109 |
|
| 110 |
## Run the controls first
|
| 111 |
|
|
@@ -113,25 +130,40 @@ The two control matrices exist to prove the learned matrix is doing the work rat
|
|
| 113 |
|
| 114 |
| Matrix | Output for *"a red ball on a wood table"* |
|
| 115 |
|---|---|
|
| 116 |
-
| `
|
| 117 |
-
| `
|
| 118 |
-
|
|
| 119 |
|
| 120 |
`βW_identityβ = 50.6` against `βW_learnedβ = 52.4` β near-identical energy, so the difference is structural, not a matter of scale.
|
| 121 |
|
| 122 |
**If the identity control ever looks fine, the learned matrix adds nothing β and you want to know that before trusting it.**
|
| 123 |
|
| 124 |
-
##
|
| 125 |
|
| 126 |
-
|
| 127 |
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
|
| 132 |
-
**
|
| 133 |
|
| 134 |
-
**
|
| 135 |
|
| 136 |
## Required models
|
| 137 |
|
|
@@ -139,7 +171,7 @@ Note that this does not apply to **ref2va**: there the identity comes from the r
|
|
| 139 |
|---|---|
|
| 140 |
| Diffusion model + VAEs | [Comfy-Org/MiniMax-H3](https://huggingface.co/Comfy-Org/MiniMax-H3) |
|
| 141 |
| Text encoder, 4B | [Comfy-Org/Krea-2](https://huggingface.co/Comfy-Org/Krea-2) β `text_encoders/qwen3vl_4b_fp8_scaled.safetensors` |
|
| 142 |
-
| Text encoder, 8B | any ComfyUI-format Qwen3-VL-8B (the 8B
|
| 143 |
|
| 144 |
The 32B text encoder is **no longer needed** β that is the entire point.
|
| 145 |
|
|
@@ -158,4 +190,4 @@ You remain responsible for what you generate and for complying with the licences
|
|
| 158 |
|
| 159 |
## Credits
|
| 160 |
|
| 161 |
-
Vibe-coded with **Anthropic Claude Code (Opus 5)**. Every number quoted was measured on real hardware, not estimated: where a prediction turned out wrong, the measurement won and the text was corrected.
|
|
|
|
| 12 |
library_name: comfyui
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# ClipProj β MiniMax H3 conditioning from a Qwen3-VL-4B or 8B
|
| 16 |
|
| 17 |
+
**Projection matrices that let a small Qwen3-VL replace the Qwen3-VL-32B text encoder of MiniMax H3.**
|
| 18 |
|
| 19 |
+
**15.7 GB β 4.5 GB of VRAM**, with no change to the diffusion model, the VAEs or the sampler.
|
| 20 |
|
| 21 |
> β οΈ **Proof of concept β working, but a proof of concept.** It runs and produces good video, and every number below was measured on real hardware. Built and tested on a single setup (Windows 11, NVIDIA, ComfyUI 0.31.0) with deliberately limited exploration.
|
| 22 |
|
| 23 |
These files are useless on their own. They require the custom node:
|
| 24 |
**[github.com/nicolab28/ComfyUI-ClipProj](https://github.com/nicolab28/ComfyUI-ClipProj)**
|
| 25 |
|
| 26 |
+
## Update to node 0.1.3, and re-download
|
| 27 |
|
| 28 |
+
Two reasons, one of them silent.
|
| 29 |
|
| 30 |
+
**The `-mlp` matrices carry a residual network, and an older node ignores it without saying so.** It reads the matrix, finds keys it does not know, drops them, and applies the linear part alone. Nothing fails, nothing warns, and you end up judging the plain matrix while believing you tested the residual. Node 0.1.3 reads them.
|
| 31 |
|
| 32 |
+
**Everything is renamed.** The old `h3_qwen3vl_*` files have moved to `obsolete/` and the `.pt` copies are gone: opening a pickle executes code, which makes no sense for a file holding six tensors. If a workflow of yours names an old file, either point it at `obsolete/` or, better, switch to the new set.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
## What this is
|
| 35 |
|
| 36 |
+
MiniMax H3 conditions on a Qwen3-VL-32B truncated to 50 layers β 15.7 GB in NVFP4 β solely to turn a prompt into a `[seq, 5120]` tensor. This repository provides a learned map that lets a much smaller Qwen3-VL produce the same conditioning:
|
| 37 |
|
| 38 |
```
|
| 39 |
cond = ((h - mean_in) / std_in) @ W * std_out + mean_out
|
| 40 |
```
|
| 41 |
|
| 42 |
+
and, in the `-mlp` files, plus the output of a small residual network fed the same standardised input.
|
| 43 |
+
|
| 44 |
+
It works because every Qwen3-VL shares the **same tokenizer** (151936 tokens): a prompt yields the same tokens at the same positions in both models, so a position-by-position mapping between their hidden states can be learned. The matrix is fitted by plain **ridge regression** β no gradients, no epochs, no learning rate. The residual network is the only part that is trained.
|
| 45 |
|
| 46 |
## Files
|
| 47 |
|
| 48 |
Put them in `ComfyUI/models/clip_projections/`.
|
| 49 |
|
| 50 |
+
**Start with `mmh3-8b-ClipProj-celeb-mlp` if you have the VRAM, `mmh3-4b-ClipProj-celeb-mlp` otherwise.**
|
| 51 |
|
| 52 |
+
| File | Encoder | Names covered | Residual | Test cosine |
|
| 53 |
|---|---|---|---|---|
|
| 54 |
+
| `mmh3-4b-ClipProj` | any Qwen3-VL-4B | no | no | 0.7169 |
|
| 55 |
+
| `mmh3-4b-ClipProj-mlp` | any Qwen3-VL-4B | no | yes | 0.7944 |
|
| 56 |
+
| `mmh3-4b-ClipProj-celeb` | any Qwen3-VL-4B | **yes** | no | 0.7095 |
|
| 57 |
+
| `mmh3-4b-ClipProj-celeb-mlp` | any Qwen3-VL-4B | **yes** | yes | 0.7930 |
|
| 58 |
+
| `mmh3-8b-ClipProj` | any Qwen3-VL-8B | no | no | 0.7528 |
|
| 59 |
+
| `mmh3-8b-ClipProj-mlp` | any Qwen3-VL-8B | no | yes | 0.7970 |
|
| 60 |
+
| `mmh3-8b-ClipProj-celeb` | any Qwen3-VL-8B | **yes** | no | 0.7466 |
|
| 61 |
+
| `mmh3-8b-ClipProj-celeb-mlp` | any Qwen3-VL-8B | **yes** | yes | **0.8037** |
|
| 62 |
+
| `mmh3-ClipProj-control-zero` | β | control, run it once | β | β |
|
| 63 |
+
| `mmh3-ClipProj-control-identity` | β | control, run it once | β | β |
|
| 64 |
|
| 65 |
+
All eight are calibrated on the same general corpus and measured on the same held-out prompts, so the column is comparable across every row.
|
| 66 |
|
| 67 |
+
Every matrix works on **any variant of its own size**: the measured cosine gap between a bf16-calibrated matrix applied to an abliterated fp8 encoder is 0.0023. You do not need the exact checkpoint a matrix was calibrated on. The 8B matrices need an 8B encoder though β 4096 input dimensions instead of 2560 β and the node checks the width and refuses a mismatch.
|
| 68 |
|
| 69 |
+
## Named people
|
| 70 |
|
| 71 |
+
**This is what changed in 0.1.3, and it was a corpus problem.**
|
| 72 |
|
| 73 |
+
The calibration corpus named a person on about 70 lines out of 8632, roughly 0.02 % of the training tokens. The directions of the hidden space that carry an identity were therefore constrained by nothing at all, and the fit put whatever minimised the error on landscape descriptions there. Named people came out as somebody else.
|
| 74 |
|
| 75 |
+
The `-celeb` matrices add 500 people, ranked by popularity, with five short prompts and two long ones each. What it buys and what it costs:
|
| 76 |
|
| 77 |
+
| | name tokens | rest of the sentence | general test set |
|
| 78 |
+
|---|---|---|---|
|
| 79 |
+
| without | 0.8265 | 0.9358 | 0.7944 |
|
| 80 |
+
| with | **0.8844** | **0.9516** | 0.7930 |
|
| 81 |
|
| 82 |
+
Seven thousandths of cosine on the general corpus, for six points on the tokens that carry an identity. The rest of the sentence improves too, because the celebrity prompts are short and the general corpus had nothing under fifteen words.
|
| 83 |
|
| 84 |
+
Two findings that decide how far this is worth pushing.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
**Two contexts per person are enough.** Measured on contexts held out for people the matrix had seen: 0.9875 at two, 0.9945 at five, 0.9986 at twenty. Forty is a waste.
|
| 87 |
|
| 88 |
+
**Five hundred names generalise to names never seen.** A held-out band at popularity ranks 501 to 540, absent from every calibration, reconstructs at 0.8795 against 0.8844 for the covered ones. Covering 500 people does not teach 500 names; it teaches the map how to handle that region of the space. Going to several thousand would buy very little.
|
| 89 |
|
| 90 |
+
**What still fails is not the corpus.** Characters whose identity is a mask rather than a face come out as a stranger wearing the right costume. People whose fame predates the era when everything was photographed come out wrong or generic. And some names fail on the plain 32B too, so run the reference before blaming the projection β that check has overturned three of my own conclusions.
|
| 91 |
+
|
| 92 |
+
## Where the calibration data comes from
|
| 93 |
+
|
| 94 |
+
The general corpus is [GokuScraper/seedance-2-prompts-datasets](https://huggingface.co/datasets/GokuScraper/seedance-2-prompts-datasets), filtered to prompts of fifteen words or more and deduplicated: 8632 lines, median 128 words. The 500 named people come from a TMDB export published on Kaggle, ranked by popularity, with transliterated names dropped beyond rank 1000.
|
| 95 |
+
|
| 96 |
+
Around each name, five short prompts are generated from templates, and two longer ones in MiniMax H3's section format are written by Mistral Small and Gemini Flash Lite, half each. Everything needed to rebuild the corpus is in the node's `calibration/` folder, including the system prompt the long prompts were written from.
|
| 97 |
+
|
| 98 |
+
## The residual network
|
| 99 |
+
|
| 100 |
+
The `-mlp` files carry a `d_in β 16384 β 5120` network with a GELU, added to the matrix rather than replacing it. Its last layer is initialised to zero, so at the first step the model reproduces the matrix exactly and can only improve on it. It is worth 0.05 to 0.08 of cosine, four times what multiplying the corpus by eleven buys the linear map.
|
| 101 |
+
|
| 102 |
+
**Which of the two renders better is not settled.** The cosine does not predict it β that is the single most repeated lesson of this project. Try both on your own prompts.
|
| 103 |
+
|
| 104 |
+
Two things measured while building it. Width beats depth: at equal parameter count, two hidden layers of 8192 reach 0.7691 against 0.7944 for one layer of 16384. And a residual extrapolates worse than a matrix does β outside the corpus it saw, a linear map degrades gracefully while the network collapses.
|
| 105 |
|
| 106 |
## Measured results
|
| 107 |
|
| 108 |
+
| | 4B | 8B |
|
| 109 |
+
|---|---|---|
|
| 110 |
+
| matrix, no names | 0.7169 | 0.7528 |
|
| 111 |
+
| matrix + residual | 0.7944 | 0.7970 |
|
| 112 |
+
| matrix, names covered | 0.7095 | 0.7466 |
|
| 113 |
+
| matrix + residual, names covered | 0.7930 | **0.8037** |
|
| 114 |
+
|
| 115 |
+
A cosine of 0.79 sounds poor and is not β the DiT tolerates far more than the metric suggests. What holds up in actual generation: simple prompts, structured multi-shot prompts with several distinct cuts and no bleed between them, fl2va with first and last frame, ref2va with a reference image, and since 0.1.3 ref2va with a reference video.
|
| 116 |
+
|
| 117 |
+
Fidelity does **not** collapse on short prompts: measured per-token cosine goes from 0.937 at 80 words to 0.908 at 2 words, once the attention sink is handled.
|
| 118 |
|
| 119 |
+
## Speech
|
| 120 |
|
| 121 |
+
The first release lost non-English speech: a French line came out half Spanish, and the 8B put everything in English. That was the clearest regression and I could not explain it then.
|
| 122 |
|
| 123 |
+
With `mmh3-8b-ClipProj-celeb-mlp`, a three-shot clip carrying English, French and Spanish comes out like the 32B does, and the audio level gap measured against the reference has gone from 7.6 dB to 3.5.
|
| 124 |
+
|
| 125 |
+
Part of what was blamed on the projection was not the projection. A line that fills more than about two thirds of its shot comes out slurred whatever encoder produced the conditioning β the fix is a longer shot, not a better matrix. And MiniMax H3 expects speech wrapped in `<d>[Language] ...</d>` with a stable speaker id declared beforehand; without that, one voice with one accent is used for the whole clip. Neither of those is documented here because neither is ours, but both cost me a day.
|
| 126 |
|
| 127 |
## Run the controls first
|
| 128 |
|
|
|
|
| 130 |
|
| 131 |
| Matrix | Output for *"a red ball on a wood table"* |
|
| 132 |
|---|---|
|
| 133 |
+
| `mmh3-ClipProj-control-zero` | a countryside landscape β the prompt is entirely ignored |
|
| 134 |
+
| `mmh3-ClipProj-control-identity` | a golden object in flames β unusable |
|
| 135 |
+
| a learned matrix | the red ball on a wood table |
|
| 136 |
|
| 137 |
`βW_identityβ = 50.6` against `βW_learnedβ = 52.4` β near-identical energy, so the difference is structural, not a matter of scale.
|
| 138 |
|
| 139 |
**If the identity control ever looks fine, the learned matrix adds nothing β and you want to know that before trusting it.**
|
| 140 |
|
| 141 |
+
## What is in obsolete/
|
| 142 |
|
| 143 |
+
The previous matrices, kept because a comparison posted on r/StableDiffusion ran on them and the links have to keep working. They have no name coverage and are calibrated on a corpus thirty times smaller. There is no reason to prefer them.
|
| 144 |
|
| 145 |
+
Among them, the `CONDPROJ` pair, and the story is worth telling because the mistake was instructive.
|
| 146 |
+
|
| 147 |
+
The DiT does not consume the conditioning as it arrives: it first passes it through `condition_proj`, a `Linear(5120 β 5376)` feeding the token refiner. That layer's spectrum is very uneven β a factor of 45 between the top and bottom deciles of its singular values, 52 % of the energy in 10 % of the directions. Plain ridge regression ignores this and spends as much effort on a direction the DiT will multiply by 0.10 as on one it will multiply by 37. Calibrating against the **output** of that layer instead, then mapping back through the pseudo-inverse, should therefore minimise the error the DiT actually sees. The cosine went from 0.697 to 0.845 on the 4B and 0.731 to 0.860 on the 8B.
|
| 148 |
+
|
| 149 |
+
Then I compared what the two matrices actually output:
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
4B CONDPROJ against unweighted, same corpus cosine 0.999998
|
| 153 |
+
8B CONDPROJ against unweighted, same corpus cosine 0.999999
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
They are the same function. Unregularised least squares is invariant to an invertible linear transform of the targets, so fitting in one space and mapping back recovers the same map; only the ridge penalty breaks that invariance, and with 37 851 training tokens against Ξ» = 1000 it barely binds. The entire gain was an artefact of measuring in a different space.
|
| 157 |
+
|
| 158 |
+
*The idea came from u/stddealer on r/StableDiffusion, and it was a good one. The measurement is on me: I published the cosine before checking whether the matrix had changed at all.*
|
| 159 |
+
|
| 160 |
+
## Known limitations
|
| 161 |
|
| 162 |
+
**Quantisation costs facts.** Comparing `int8_convrot` against `bf16` on factual recall shows errors appearing under quantisation. Fine for general use, worth knowing if your prompts lean on proper nouns.
|
| 163 |
|
| 164 |
+
**Masks defeat identity.** A character recognised by a costume rather than a face comes out as an unknown person in the right suit. No corpus fixes that, because the identity is not in the name's representation to begin with.
|
| 165 |
|
| 166 |
+
**Counting is unreliable, and not because of the projection.** Ask for three of something and you get four, on the 32B too. Enumerating works better than announcing a number.
|
| 167 |
|
| 168 |
## Required models
|
| 169 |
|
|
|
|
| 171 |
|---|---|
|
| 172 |
| Diffusion model + VAEs | [Comfy-Org/MiniMax-H3](https://huggingface.co/Comfy-Org/MiniMax-H3) |
|
| 173 |
| Text encoder, 4B | [Comfy-Org/Krea-2](https://huggingface.co/Comfy-Org/Krea-2) β `text_encoders/qwen3vl_4b_fp8_scaled.safetensors` |
|
| 174 |
+
| Text encoder, 8B | any ComfyUI-format Qwen3-VL-8B (the 8B matrices expect 4096 input dims) |
|
| 175 |
|
| 176 |
The 32B text encoder is **no longer needed** β that is the entire point.
|
| 177 |
|
|
|
|
| 190 |
|
| 191 |
## Credits
|
| 192 |
|
| 193 |
+
Vibe-coded with **Anthropic Claude Code (Opus 5)**. Every number quoted was measured on real hardware, not estimated: where a prediction turned out wrong, the measurement won and the text was corrected. Three claims in the previous version of this file were wrong and are corrected here.
|