- sasori TQ3P — FLUX.2 [klein] 9B ternarizado (K=3 trit-planes)
- Arquivos
- Números medidos
- VRAM e footprint (medidos)
- Velocidade de denoise — s/it em regime permanente (
sd-server, modelo já carregado) - Velocidade em CPU — o veredito se INVERTE
- Fidelidade — CLIP-score pareado (ViT-L/14, N=16 pares: 8 prompts × 2 seeds)
- Detalhe de alta frequência — a métrica que o CLIP-score NÃO vê
- Leitura honesta destes números
- Como rodar
- O que fica em FP16 (e por quê)
- Licença
- Citação / proveniência
- Arquivos
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: 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 emkernel/— são 25 hunks, aplicam limpo, e o build leva ~5 min. Veja 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.safetensorsdeblack-forest-labs/FLUX.2-small-decoder(o que foi usado em todas as medições; sem gate de licença). A VAE oficialflux2_ae.safetensorsvive no repo gatedFLUX.2-deve 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:
- 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).
- 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 oQ4_Kpassa pelo kernelmmq, 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 oQ4_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 oQ4_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, throughput | TQ3P (este repo) | 1,52× mais rápido que o Q4_K_M, empata com o FP16, 2,44× menos bytes que o FP16 |
| GPU, detalhe máximo | Q4_K_M |
preserva 97,9 % da energia de detalhe contra 92,0 % do TQ3P |
| CPU | Q4_K_M (unsloth) |
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.
Detalhe de alta frequência — a métrica que o CLIP-score NÃO vê
O CLIP-score mede alinhamento imagem-texto; ele não penaliza perda de micro-textura. Medindo os mesmos 16 pares com energia de detalhe (variância do Laplaciano) e fração de energia espectral acima de meia-Nyquist:
| braço | bpw | energia de detalhe | vs FP16 | energia de alta freq. | pares abaixo do FP16 |
|---|---|---|---|---|---|
| FP16 | 16 | 734,2 | 100 % | 100 % | 0/16 |
| Q4_K_M | ~4,5 | 718,4 | 97,9 % | 97,7 % | 10/16 |
| TQ3P (K=3) | 6,1875 | 675,3 | 92,0 % | 95,4 % | 13/16 |
| TQ2P (K=2) | 4,125 | 345,3 | 47,0 % | 68,7 % | 16/16 |
Isto qualifica o TQ3P para baixo e é preciso dizer: por detalhe fino o Q4_K_M preserva mais
(97,9 %) que o TQ3P (92,0 %) — o oposto do que o CLIP-score sugeria (99,0 % vs 99,6 %). A perda de
8 % do TQ3P é modesta e não aparece a olho nu nas amostras, mas existe e é medida.
E fecha a porta para o K=2, que testamos exatamente porque a 4,125 bpw ele é menor que o
Q4_K_M e seria o único ponto onde este formato poderia dominar o baseline: o K2 é 12 % menor
(4,85 vs 5,50 GiB) e 1,48× mais rápido que o Q4_K_M na GPU — mas perde 53 % da energia de detalhe,
em 16/16 pares. Visualmente: textura de pele alisada, e cenas que perdem elementos acessórios
(torneira, ralo, reflexos). O CLIP-score do K2 ficou em 101,6 % do FP16 — acima do FP — porque
uma imagem mais "canônica" do prompt, com menos conteúdo secundário, é premiada por essa métrica.
Dissociação de manual: a métrica subiu enquanto a imagem piorou. Por isso o K2 não é publicado
aqui: o Q4_K_M o domina em qualidade-por-byte.
Lição de método: para difusão, CLIP-score sozinho não sustenta uma afirmação de fidelidade. Um par CLIP-score + energia de alta frequência já separa "diz o prompt" de "tem a textura".
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 escolheQ4_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, e sozinho ele engana. Mede alinhamento imagem-texto pareado, não fidelidade perceptual. Medido aqui: o K=2 pontua acima do FP16 no CLIP-score enquanto perde 53 % da energia de detalhe — ver a seção de alta frequência. 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--mmapsob RAM apertada, e a fidelidade da edição (ela funciona — foi exercitada no webapp interno — mas não foi avaliada quantitativamente).
Como rodar
# 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-cpuse 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.00MBno log) — medido aqui: com--offload-to-cpuo log mostraVRAM 0.00MBe 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 <núcleos>. Medido (ver a seção de CPU acima): aqui oQ4_K_Mganha por 1,51×; a vantagem de ~1,5× do TQ3P é exclusivamente de GPU. - Servir com o modelo quente:
sd-servermantém os pesos carregados; viasd-clicada 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:
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 <path do stable-diffusion.cpp buildado> -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)
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) eunsloth/Qwen3-8B-GGUF(Qwen3-8B-BF16.gguf) — GGUFs full-precision, sem quantização em cascata. - Runtime:
leejet/stable-diffusion.cppmaster-802-e92e86f, ggmleced84c8+ 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.
- Downloads last month
- 460
We're not able to determine the quantization variants.