FardoX's picture
mede CPU (o veredito se INVERTE: Q4_K_M ganha 1,46-1,51x) e documenta `sasori draw`
f03b346 verified
|
Raw
History Blame
17.1 kB
metadata
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: 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 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) 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

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