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 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):

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, 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 FP16acima 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

# 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 <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:

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) 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.
Downloads last month
460
GGUF
Model size
9B params
Architecture
flux
Hardware compatibility
Log In to add your hardware

We're not able to determine the quantization variants.

Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for FardoX/sasori-flux2-klein-9b-tq3p

Finetuned
Qwen/Qwen3-8B
Quantized
(377)
this model