--- license: other license_name: flux-2-klein-non-commercial-license license_link: https://huggingface.co/black-forest-labs/FLUX.2-klein-9B/blob/main/LICENSE.md base_model: - black-forest-labs/FLUX.2-klein-9B - Qwen/Qwen3-8B pipeline_tag: text-to-image tags: - text-to-image - flux - flux.2 - gguf - quantization - ternary - sasori - stable-diffusion-cpp library_name: sasori --- # sasori TQ3P — FLUX.2 [klein] 9B ternarizado (K=3 trit-planes) **O que é:** o denoiser (DiT) do FLUX.2 [klein] 9B e o seu text encoder (Qwen3-8B) convertidos para **TQ3P**, o formato ternário multi-plano do [sasori](https://github.com/Adriel007/PhD-propose): cada peso é a soma de **3 planos de trits** `{-1,0,+1}` com escala f16 por grupo de 256 — **6,1875 bits por peso**. Sem retreino, sem dados de calibração (data-free, layer-local). > ### ⚠️ Precisa de um runtime patchado — não roda no stable-diffusion.cpp oficial > `TQ3P` é um tipo de quantização **customizado do ggml** (type id 43) que o upstream não conhece: um > binário sem patch **recusa a carregar** estes arquivos. Os patches e o script de build estão em > [`kernel/`](./kernel) — são 25 hunks, aplicam limpo, e o build leva ~5 min. Veja > [Como rodar](#como-rodar). ## Arquivos | arquivo | o que é | tamanho | |---|---|---| | `flux-2-klein-9b-TQ3P.gguf` | denoiser DiT, K=3 (112 tensores ternários / 89 em FP16) | **6,94 GiB** | | `text_encoder/Qwen3-8B-TQ3P.gguf` | text encoder, K=3 | **7,33 GiB** | | `kernel/sdcpp-cpu.patch` | tipo TQ{K}P no ggml-CPU (bloco, `vec_dot` AVX2 `maddubs`) | 17 hunks | | `kernel/sdcpp-cuda.patch` | gate CUDA (`dequant→cuBLAS`) para o denoiser rodar na GPU | 8 hunks | | `kernel/build.sh` · `kernel/BUILD.md` | build reproduzível do runtime | — | | `bench/` | os JSONs de todas as medições abaixo | — | | `samples/` | amostras pareadas FP16 vs TQ3P (mesmo prompt, mesma seed) | — | **Você também precisa** (não redistribuídos aqui): - **VAE**: `full_encoder_small_decoder.safetensors` de [`black-forest-labs/FLUX.2-small-decoder`](https://huggingface.co/black-forest-labs/FLUX.2-small-decoder) (o que foi usado em todas as medições; sem gate de licença). A VAE oficial `flux2_ae.safetensors` vive no repo gated [`FLUX.2-dev`](https://huggingface.co/black-forest-labs/FLUX.2-dev) e também serve. ## Números medidos Tudo medido **nesta conversão**, num pod A100-80GB PCIe, `sd.cpp master-802-e92e86f` + os patches deste repo, CUDA sm_80, 1024×1024, 4 steps (o klein-9B é step-distilled), euler, `--diffusion-fa`. ### VRAM e footprint (medidos) | braço | DiT (arquivo) | text encoder (arquivo) | **pacote** | VRAM medida (1024²) | |---|---|---|---|---| | FP16 / BF16 (16 bpw) | 16,91 GiB | 15,26 GiB | **32,17 GiB** | 33 058 MB | | DiT TQ3P + TE BF16 | 6,94 GiB | 15,26 GiB | 22,20 GiB | 22 853 MB | | **DiT TQ3P + TE TQ3P** (6,1875 bpw) | **6,94 GiB** | **7,33 GiB** | **14,27 GiB** | **14 729 MB** | | Q4_K_M + Q8_0 (baseline) | 5,50 GiB | 8,11 GiB | 13,61 GiB | 13 425 MB | ### Velocidade de denoise — s/it em regime permanente (`sd-server`, modelo já carregado) 5 requests por braço e por resolução, **descartando o primeiro** (warmup), lendo o `sampling completed` do próprio log do servidor (isola o denoise: exclui load, encode do prompt e decode da VAE). Os três braços rodam **no mesmo pod, ao mesmo tempo**, cada um num `sd-server`. | braço | s/it @ 1024² | s/it @ 768² | vs FP16 @1024² | vs Q4_K_M @1024² | |---|---|---|---|---| | FP16 | 0,853 | 0,460 | — | 1,54× mais rápido | | **TQ3P (K=3)** | **0,865** | 0,487 | 1,4 % mais lento | **1,52× mais rápido** | | Q4_K_M | 1,312 | 0,752 | 54 % mais lento | — | **Duas leituras, ambas medidas:** 1. **TQ3P empata com o FP16** (1,4 % a 1024², 5,9 % a 768² — dentro da variação entre resoluções), com **2,44× menos bytes** no DiT. Isso reproduz o que o projeto já havia medido no SD3.5-Medium (TQ3P 0,343 vs FP 0,347 s/it). 2. **TQ3P é ~1,5× mais rápido que o Q4_K_M**, que é o braço **mais lento** apesar de ser o **menor** arquivo. **Hipótese** (não isolada aqui): o gate CUDA do sasori roteia TQ{K}P por `dequant → cuBLAS`, o mesmo caminho de GEMM do FP16 — enquanto o `Q4_K` passa pelo kernel `mmq`, que neste regime (denoise de ~4096 tokens de imagem por passo) rende menos. Falsificar exigiria comparar os caminhos de kernel isoladamente, o que **não foi feito**. > **Correção de um número que esta página publicou antes.** A primeira versão deste card dizia que o > TQ3P era "1,38× mais rápido que o FP16", medido com o `sd-cli` (uma invocação por imagem, 16 > gerações). Aquela medição estava **contaminada por overhead de setup por invocação** — nela o > `Q4_K_M` (o menor modelo) aparecia quase tão lento quanto o FP16, o que não se sustenta em regime > permanente. Com o modelo carregado uma vez e 5 repetições, a comparação correta é a tabela acima: > **empate com o FP16**, e vantagem sobre o `Q4_K_M`. O número anterior estava errado e foi retirado. ### Velocidade em CPU — o veredito se INVERTE EPYC 4564P (16 vCPU, AVX2/AVX512), 124 GB de RAM (folgada de propósito: mede o **teto**, sem `--mmap` nem releitura de disco), 4 steps, `-t 16`, executado via `sasori draw`. **512²** | braço | encode do prompt | sampling | **s/it** | VAE decode | total por imagem | |---|---|---|---|---|---| | TQ3P (DiT K3 + TE K3) | 17,51 s | 259,43 s | **64,86** | 9,43 s | ~4,8 min | | Q4_K_M + Q8_0 | 12,81 s | 171,32 s | **42,83** | 9,46 s | ~3,2 min | **1024²** | braço | encode do prompt | sampling | **s/it** | VAE decode | total por imagem | |---|---|---|---|---|---| | TQ3P (DiT K3 + TE K3) | 17,19 s | 833,03 s | **208,26** | 35,89 s | ~14,8 min | | Q4_K_M + Q8_0 | 12,17 s | 570,16 s | **142,54** | 36,90 s | ~10,3 min | **Em CPU o `Q4_K_M` é 1,51× (512²) / 1,46× (1024²) MAIS RÁPIDO que o TQ3P — o inverso quase exato da GPU** (onde o TQ3P era 1,52× mais rápido). A simetria não é coincidência: em CPU o denoise é limitado por **bytes lidos por peso**, e o K3 a 6,1875 bpw lê ~37 % mais que o `Q4_K_M` a ~4,5 bpw; na GPU o gargalo é o caminho de kernel, e ali o TQ3P entra pelo `dequant → cuBLAS`. O text encoder ternário paga o mesmo preço (17,5 s vs 12,8 s no encode do prompt). Isto reproduz e reforça o que o projeto já havia medido no SD3.5-Medium em CPU (TQ3P 24,0 vs Q4_K_M 18,9 s/it). **Recomendação honesta, por hardware:** | onde você roda | escolha | por quê | |---|---|---| | **GPU** | **TQ3P (este repo)** | 1,52× mais rápido que o `Q4_K_M`, empata com o FP16, 2,44× menos bytes que o FP16 | | **CPU** | **`Q4_K_M`** ([unsloth](https://huggingface.co/unsloth/FLUX.2-klein-9B-GGUF)) | 1,51× mais rápido que este artefato; ternário não é o formato certo aí | E, em qualquer hardware, **CPU não é um caminho confortável para o 9B**: no melhor caso (RAM folgada, sem `--mmap`) são ~3–5 min por imagem a 512² e **10–15 min a 1024²**. Numa máquina de 16 GB, onde o pacote de 14,3 GB só entra com `--mmap`, é pior que isso. Para CPU o alvo razoável é o **klein-4B**, 2,3× menor e com licença Apache-2.0. ### Fidelidade — CLIP-score pareado (ViT-L/14, N=16 pares: 8 prompts × 2 seeds) | braço | CLIP-score | retenção vs FP16 | Wilcoxon p | placar par-a-par (FP/braço) | |---|---|---|---|---| | FP16 | 0,7303 ± 0,0589 | — | — | — | | **DiT TQ3P** | 0,7285 ± 0,0702 | **99,8 %** | 0,744 (n.s.) | **8/8 — empate** | | **DiT TQ3P + TE TQ3P** | 0,7276 ± 0,0631 | 99,6 % | 0,464 (n.s.) | 8/8 — empate | | Q4_K_M + Q8_0 | 0,7227 ± 0,0608 | 99,0 % | 0,231 (n.s.) | 9/7 | Inspeção visual das amostras pareadas (`samples/`): nos 4 braços a contagem do prompt "three yellow rubber ducks" sai correta e o fotorrealismo se mantém; o que muda é a trajetória de denoise (composição levemente diferente), não a qualidade. ### Leitura honesta destes números - **O que o TQ3P entrega, em uma frase:** velocidade de denoise **igual à do FP16** (dentro de 1,4–5,9 %) com **2,44× menos bytes** no DiT, e **~1,5× mais rápido que o `Q4_K_M`**. - **O TQ3P não é dominado pelo Q4_K_M, mas também não o domina.** O DiT ternário é **26 % maior** que o `Q4_K_M` (6,94 vs 5,50 GiB) e ~1,5× mais rápido. Como *pacote* (DiT + text encoder) a diferença de tamanho quase desaparece — 14,27 vs 13,61 GiB — porque o Qwen3-8B em TQ3P (7,33 GiB) fica **menor** que em Q8_0 (8,11 GiB). Quem precisa do menor arquivo escolhe `Q4_K_M`; quem precisa de throughput com footprint reduzido escolhe TQ3P. - **Nenhuma diferença de fidelidade é detectável a N=16** — em nenhum braço, inclusive o baseline. Isso **não** é o mesmo que "TQ3P = FP16": N=16 é subpotente para um Δ de ~0,2 pp. O que se pode afirmar é mais fraco e mais honesto: **o placar par-a-par do TQ3P contra o FP16 é 8/8**, sem tendência direcional (no run do SD3.5 o FP ganhava 11/16), e a inspeção visual não mostra degradação. - **CLIP-score não é FID.** Mede alinhamento imagem-texto pareado, não fidelidade perceptual da distribuição. Nenhum FID de Fréchet foi computado. - **Escopo da medição:** 1 modelo, 4 steps, euler, VAE `small-decoder`. **GPU** (A100-80 PCIe): 768² e 1024², 5 repetições em regime permanente. **CPU** (EPYC 4564P, 16 vCPU): 512², 1 execução por braço, RAM folgada. Fidelidade: 1024² na GPU, 2 seeds, 8 prompts, 1 CLIP (ViT-L/14), N=16 pares. Não medido: ARM, outros samplers, FID, o regime com `--mmap` sob RAM apertada, e a fidelidade da **edição** (ela funciona — foi exercitada no webapp interno — mas não foi avaliada quantitativamente). ## Como rodar ```bash # 1) runtime com o kernel ternário (~5 min) git clone https://huggingface.co/FardoX/sasori-flux2-klein-9b-tq3p && cd sasori-flux2-klein-9b-tq3p CUDA_ARCH=86 bash kernel/build.sh # 80=A100 86=3090 89=4090 90=H100; BUILD_CUDA=OFF p/ CPU strings stable-diffusion.cpp/build/bin/sd-cli | grep TQ3P # sanity: o tipo tem que existir # 2) pesos (este repo + a VAE) hf download FardoX/sasori-flux2-klein-9b-tq3p --local-dir . hf download black-forest-labs/FLUX.2-small-decoder full_encoder_small_decoder.safetensors --local-dir vae # 3) gerar stable-diffusion.cpp/build/bin/sd-cli \ --diffusion-model flux-2-klein-9b-TQ3P.gguf \ --llm text_encoder/Qwen3-8B-TQ3P.gguf \ --vae vae/full_encoder_small_decoder.safetensors \ -p "a lovely cat sitting on a wooden table, soft window light" \ --cfg-scale 1.0 --steps 4 -W 1024 -H 1024 --sampling-method euler \ --diffusion-fa -o cat.png -v ``` ### Paralelismo — o que realmente acelera - **`--diffusion-fa`** (flash-attention no denoiser) e **4 steps**: o klein-9B é step-distilled, então 4 steps é a operação normal, não um atalho. - **NÃO passe `--offload-to-cpu` se você tem VRAM.** A doc oficial do sd.cpp usa essa flag em todos os exemplos, e ela põe **todos os pesos na RAM** (`VRAM 0.00MB` no log) — medido aqui: com `--offload-to-cpu` o log mostra `VRAM 0.00MB` e o sampling de 4 steps a 512² leva **27,8 s**; sem a flag, a 1024² (4× mais pixels) leva **3,46 s** de sampling (4,21 s de request completo). Use a flag só se o modelo não couber na sua GPU. - **VRAM necessária** (TQ3P, 1024², sem offload): **14,7 GB** com o text encoder também em TQ3P (7,1 GB o DiT + 7,5 GB o TE + 0,1 GB a VAE), ou 22,9 GB mantendo o text encoder em BF16. - **CPU**: o kernel TQ{K}P usa `_mm256_maddubs_epi16` (AVX2) e é um mpGEMM direto — o trit multiplica sem dequantizar. Passe `-t `. **Medido** (ver a seção de CPU acima): aqui o `Q4_K_M` ganha por 1,51×; a vantagem de ~1,5× do TQ3P é **exclusivamente de GPU**. - **Servir com o modelo quente:** `sd-server` mantém os pesos carregados; via `sd-cli` cada imagem paga load + setup outra vez (o que também distorce medição de velocidade — veja a nota de correção acima). Para uso repetido, use o servidor. ### `sasori draw` — deixa a ferramenta escolher as flags de memória O `stable-diffusion.cpp` tem várias flags de residência de peso (`--mmap`, `--offload-to-cpu`, `--clip-on-cpu`, `--vae-on-cpu`, `--stream-layers`, `--max-vram`) e saber **qual usar em qual tamanho** é chato. O formato TQ{K}P decide quantos *bytes* um peso custa; quem decide o que fica residente e quando é o runtime — então o sasori calcula o encaixe e escolhe: ```bash python3 -m sasori draw flux-2-klein-9b-TQ3P.gguf \ --llm text_encoder/Qwen3-8B-TQ3P.gguf \ --vae vae/full_encoder_small_decoder.safetensors \ -p "a lovely cat sitting on a wooden table, soft window light" \ --sd-cpp -o cat.png # editar (kontext-style): o prompt passa a ser a INSTRUÇÃO python3 -m sasori draw ... -r foto.png -p "troque o fundo por uma praia ao amanhecer" # ver a contabilidade e o comando sem rodar nada python3 -m sasori draw ... --dry-run ``` Ele soma os pesos em disco + os buffers de compute que a resolução implica (coeficientes medidos: VAE **4,76 KB/px**, denoiser **1,36 KB/px**, dobrando com imagem de referência), compara com a VRAM livre / a RAM disponível de verdade, e **imprime cada flag com o motivo**. Numa GPU pequena adiciona `--offload-to-cpu` + `--clip-on-cpu`; em CPU sem RAM suficiente adiciona `--mmap` **avisando que aquilo "cabe" ao custo de reler os pesos do disco a cada step** — em vez de apresentar isso como um encaixe limpo. Exemplo real, numa máquina de 16 GB: ``` [sasori draw] pesos 14.18 GB + buffers ~6.12 GB (1024x1024) = 20.30 GB [sasori draw] ⚠ pesos+buffers 20.3 GB > RAM disponivel 11.1 GB -> --mmap: CABE, mas cada step pode reler do DISCO o que for despejado (14.2 GB de pesos). Menor resolucao ou um modelo menor e mais rapido que este modo. ``` **A VAE é o maior consumidor de buffer e não depende do tamanho do denoiser** (a mesma VAE serve o 4B e o 9B): 4 994 MB a 1024² contra ~1 250 MB a 512². Baixar a resolução é a alavanca de memória mais forte que você tem. ### Ternarizar você mesmo (e o que aprendi sobre paralelizar isso) ```bash python3 sasori/scripts/inject_tqkp_diffusion.py IN_F16.gguf OUT_TQ3P.gguf \ --K 3 --group 256 --device cuda --dry-run # audita a deny-list ANTES de gastar GPU ``` O ajuste é **layer-local e data-free** (cada matriz é independente), então a tentação é paralelizar em processos. **Medido neste run:** rodar a conversão do DiT (GPU) e a do text encoder (CPU, 32 workers) ao mesmo tempo **piorou** o caminho crítico de **3,7 s → 25 s por tensor (6,7× mais lento)**. Motivo: o solver joint-ridge na GPU fica em ~7 % de utilização; o gargalo real é o **`pack` dos trits em numpy na CPU** — os dois jobs disputavam o mesmo recurso, não recursos disjuntos. **Serializar os dois jobs na GPU, com toda a CPU para o pack de um só, foi mais rápido:** DiT 8,72 G params em 15,3 min, TE em **2,7 min** (6,94 G params). ## O que fica em FP16 (e por quê) Dos 201 tensores do DiT, **89 (354 M de 9,07 G params = 3,9 %)** continuam em FP16 — custa ~0,7 GB e protege os pontos onde um erro de trit **não** é local: | mantido FP16 | por quê | |---|---| | `*modulation*` (adaLN, 285 M) | emite shift/scale/gate por bloco a partir do timestep: um erro aqui reescala **toda** a ativação do bloco. A literatura de PTQ para DiT (Q-DiT, PTQ4DiT, ViDiT-Q) aponta adaLN/modulação como o sítio sensível. | | `txt_in` (50 M) | única porta do prompt para dentro do denoiser; ternarizar poupa ~0,1 GB e arrisca todo o condicionamento. | | `time_in` (17,8 M) | MLP de condicionamento do timestep. | | `final_layer.linear` (0,5 M) | projeção de volta ao espaço latente — mesma razão pela qual o caminho LLM do sasori mantém `output.weight` em FP. | | `img_in`, normas, `key/query_norm` | `img_in` tem in_dim 128 (não múltiplo de 256); normas são 1-D. | **Nota de rigor:** essas escolhas são **hipóteses fundamentadas** (mecanismo + literatura de PTQ de DiT), **não uma ablação medida neste run**. Ternarizar a modulação e medir o custo é o experimento seguinte — não foi feito aqui. ## Licença O denoiser é um **derivado do FLUX.2 [klein] 9B** e herda a **FLUX Non-Commercial License** da Black Forest Labs: **uso não-comercial apenas**, e as mesmas restrições se aplicam a qualquer derivado deste. Leia a licença antes de usar. O text encoder é derivado do **Qwen3-8B (Apache-2.0)**. Os patches do kernel e os scripts são do projeto sasori. ## Citação / proveniência - Pesos-fonte: `unsloth/FLUX.2-klein-9B-GGUF` (`flux-2-klein-9b-F16.gguf`) e `unsloth/Qwen3-8B-GGUF` (`Qwen3-8B-BF16.gguf`) — GGUFs full-precision, sem quantização em cascata. - Runtime: `leejet/stable-diffusion.cpp` `master-802-e92e86f`, ggml `eced84c8` + os patches deste repo. - Conversão: sasori `inject_tqkp_diffusion.py --K 3 --group 256 --device cuda`, niter=25. - Método: PTQ ternário multi-plano (joint-ridge alternado), data-free, layer-local.