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 em
kernel/ — 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.safetensors de
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 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:
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 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, 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
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.
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 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, 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 --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# 1) runtime com o kernel ternário (~5 min)2git clone https://huggingface.co/FardoX/sasori-flux2-klein-9b-tq3p &&cd sasori-flux2-klein-9b-tq3p
3CUDA_ARCH=86bash kernel/build.sh # 80=A100 86=3090 89=4090 90=H100; BUILD_CUDA=OFF p/ CPU4strings stable-diffusion.cpp/build/bin/sd-cli |grep TQ3P # sanity: o tipo tem que existir56# 2) pesos (este repo + a VAE)7hf download FardoX/sasori-flux2-klein-9b-tq3p --local-dir .8hf download black-forest-labs/FLUX.2-small-decoder full_encoder_small_decoder.safetensors --local-dir vae
910# 3) gerar11stable-diffusion.cpp/build/bin/sd-cli \12 --diffusion-model flux-2-klein-9b-TQ3P.gguf \13 --llm text_encoder/Qwen3-8B-TQ3P.gguf \14 --vae vae/full_encoder_small_decoder.safetensors \15 -p "a lovely cat sitting on a wooden table, soft window light"\16 --cfg-scale 1.0 --steps 4 -W 1024 -H 1024 --sampling-method euler \17 --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 <núcleos>. 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
1python3 -m sasori draw flux-2-klein-9b-TQ3P.gguf \2 --llm text_encoder/Qwen3-8B-TQ3P.gguf \3 --vae vae/full_encoder_small_decoder.safetensors \4 -p "a lovely cat sitting on a wooden table, soft window light"\5 --sd-cpp <path do stable-diffusion.cpp buildado> -o cat.png
67# editar (kontext-style): o prompt passa a ser a INSTRUÇÃO8python3 -m sasori draw ... -r foto.png -p "troque o fundo por uma praia ao amanhecer"910# ver a contabilidade e o comando sem rodar nada11python3 -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 --mmapavisando 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
1python3 sasori/scripts/inject_tqkp_diffusion.py IN_F16.gguf OUT_TQ3P.gguf \2 --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.cppmaster-802-e92e86f, ggml eced84c8 + os patches deste repo.