Spaces:
Paused
Paused
File size: 29,424 Bytes
3b4fb97 46d88b9 3b4fb97 46d88b9 3b4fb97 d8402ae 3b4fb97 2a0c9d8 3b4fb97 2a0c9d8 3b4fb97 2a0c9d8 3b4fb97 b4ee86a 5de0eb7 b4ee86a 7ab0dce 2a0c9d8 7ab0dce 2a0c9d8 7ab0dce 3b4fb97 5de0eb7 0a72b37 5de0eb7 0a72b37 5de0eb7 0a72b37 5de0eb7 0a72b37 5de0eb7 47520c2 89b8453 47520c2 a3b8ca7 89b8453 a3b8ca7 3b4fb97 d5ed9ba 3b4fb97 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 | """Patches cirurgicos para app.py do Space Programa.
Motivo: a configuracao atual roda Qwen3-30B-A3B em CPU com FLASH_ATTN=0 e
sem validacao de teto de threads. Este patch aplica mudancas pontuais,
verificaveis e reversiveis por env var.
Cada patch e idempotente: rodar duas vezes nao duplica nada.
Falha alto se o ponto de ancoragem nao for encontrado exatamente uma vez,
para nunca aplicar uma mudanca silenciosamente errada.
Uso:
python patch_app.py /caminho/app.py # aplica
python patch_app.py /caminho/app.py --dry-run # so mostra o diff
"""
import re
import sys
import pathlib
import difflib
PATCHES = []
def patch(name, old, new):
PATCHES.append((name, old, new))
# --------------------------------------------------------------------------
# 1. Flash attention ligado por padrao.
# Medido no proprio README: o prefill NAO e linear (1028 tok a ~159 tok/s,
# 4580 tok a ~91 tok/s). Com contexto de agente real (4.5k+ tokens) o custo
# de atencao cresce ~L^2 e domina o TTFT. FA corta exatamente esse termo.
# Em CPU-only o ganho e menor que na GPU, mas e o eixo que o autor ja tinha
# identificado e deixado desligado.
patch(
"FLASH_ATTN default on",
'FLASH_ATTN = os.environ.get("FLASH_ATTN", "0") == "1"',
'FLASH_ATTN = os.environ.get("FLASH_ATTN", "1") == "1"',
)
# --------------------------------------------------------------------------
# 2. Teto de threads: nunca passar do numero de CPUs realmente visiveis.
# Bug real: THREADS=16 fixo. Se o host entregar menos vCPU, o llama.cpp
# cria threads em excesso e o decode PIORA (contencao de spinlock). O host
# do ZeroGPU ja variou; medido nesta sessao o servidor reportava menos
# nucleos do que 16 em alguns momentos.
#
# ATENCAO: este trecho roda no IMPORT, e `log()` so e definido MAIS ABAIXO
# no arquivo. Chamar log() aqui -> NameError e o Space nao sobe.
# Por isso usa print(), que sempre existe.
patch(
"clamp THREADS to os.cpu_count()",
'THREADS = int(os.environ.get("THREADS", "16"))',
'THREADS = int(os.environ.get("THREADS", "16"))\n'
'_VISIBLE_CPUS = os.cpu_count() or 16\n'
'if THREADS > _VISIBLE_CPUS:\n'
' # print, nao log(): log() ainda nao foi definido neste ponto.\n'
' print(f"[cfg] THREADS={THREADS} > cpus visiveis={_VISIBLE_CPUS}; "\n'
' f"reduzindo para evitar contencao", flush=True)\n'
' THREADS = _VISIBLE_CPUS',
)
# --------------------------------------------------------------------------
# 3. (REMOVIDO) --no-warmup.
# Hipotese levantada em producao: DESLIGAR o warmup do llama.cpp causa uma
# janela de ~2 min apos cada restart em que o tool calling NAO funciona.
#
# Evidencia (reproduzida em dois deploys independentes):
# - logo apos o autotune terminar, 0/6 e 0/10 chamadas retornam
# finish_reason=stop sem tool_calls
# - ~2 min depois, sem tocar em nada: 10/10, 20/20, 25/25
# - o proprio diag do boot reporta tool_calls=SIM no mesmo periodo,
# entao nao e problema de template/parser
# O warmup do llama.cpp faz um forward de dummy no load que inicializa
# buffers. Sem ele, as primeiras requisicoes reais pegam estado nao
# inicializado. O ganho de --no-warmup era so um boot um pouco mais curto;
# o custo e ~2 min de tool calling quebrado apos CADA restart. Nao compensa.
# Revertido. Se a janela persistir sem --no-warmup, a causa e outra.
# --------------------------------------------------------------------------
# 4. (REMOVIDO) --cache-reuse.
# Suspeito principal da instabilidade de tool calling observada em producao.
#
# Evidencia:
# - o tool calling falha em SURTOS: mesma prompt e mesma config deram
# 0/10, depois 4/4, depois 0/6, depois 10/10, 20/20, 25/25
# - /metrics mostrou cache muito usado: prompt_cached_total=5511 contra
# prompt_total=1103 (5x mais tokens servidos do cache que processados)
# - quando falha, o modelo NAO quebra: ele responde em prosa
# ("O comando ... geralmente esta na secao scripts"), ou seja, o contexto
# que ele ve leva a outra escolha -- sintoma de contexto desalinhado
#
# `--cache-reuse N` permite reaproveitar um prefixo PARCIAL (>= N tokens)
# fazendo context shifting. Se um request casa parcialmente com uma conversa
# DIFERENTE no cache, o shift pode injetar contexto errado.
# O ganho de 68x medido no cache NAO vem daqui: vem do `--cache-prompt`,
# que ja e ligado por padrao e reaproveita prefixo EXATO. Removendo
# --cache-reuse o ganho principal continua e o risco do shift parcial sai.
#
# Se a instabilidade persistir sem --cache-reuse, a causa e outra (modelo ou
# plataforma) e deve ser reportada como tal, nao como bug de config.
# --------------------------------------------------------------------------
# 5. Defaults de geracao para CODIGO.
# MEDIDO em /props: o servidor sobe com temperature=0.8, top_p=0.95, min_p=0.05.
# Esses sao os defaults do modelo para chat generico, nao para codigo.
# 0.8 de temperatura e alto para geracao de codigo: aumenta variacao e
# invencao de API. Backends de codigo serios rodam 0.1-0.2.
#
# VERIFICADO NO BINARIO PINADO (b10985, libllama-common.so.0.4.1):
# LLAMA_ARG_TOP_K existe -> usar env
# LLAMA_ARG_TEMP NAO EXISTE
# LLAMA_ARG_TOP_P NAO EXISTE
# LLAMA_ARG_MIN_P NAO EXISTE
# Sao 143 vars LLAMA_ARG_* no build; nao ha nenhuma para temperatura/top_p,
# porque sampling e parametro de request, nao default de ambiente.
# Uma versao anterior deste patch setava LLAMA_ARG_TEMP e era NO-OP silencioso:
# o valor que importava (temperatura) nunca era aplicado.
# Correcao: temperatura e top_p vao como ARGUMENTO do servidor (--temp/--top-p),
# que afeta o default do slot sem depender do cliente.
patch(
"code-friendly generation defaults (via --temp/--top-p)",
' if "--metrics" in help_txt:\n args += ["--metrics"]',
' # Defaults de geracao para CODIGO.\n'
' # Verificado no binario b10985: NAO existe LLAMA_ARG_TEMP nem\n'
' # LLAMA_ARG_TOP_P (so LLAMA_ARG_TOP_K). Por isso temperatura e top_p\n'
' # vao por flag, nao por env -- senao viram no-op silencioso.\n'
' # O cliente continua podendo sobrescrever mandando `temperature`.\n'
' #\n'
' # VALORES: recomendacao OFICIAL do Qwen para Qwen3-Coder-30B-A3B:\n'
' # temperature=0.7, top_p=0.8, top_k=20, repetition_penalty=1.05\n'
' # (antes era 0.2/0.95, que era correcao para o default generico 0.8 do\n'
' # Qwen3-30B-A3B. Para o Coder, o autor do modelo manda 0.7 -- seguir.)\n'
' if "--temp" in help_txt or "--temperature" in help_txt:\n'
' args += ["--temp", os.environ.get("GEN_TEMP", "0.7")]\n'
' log("geracao: --temp " + os.environ.get("GEN_TEMP", "0.7") + " (coder)")\n'
' if "--top-p" in help_txt:\n'
' args += ["--top-p", os.environ.get("GEN_TOP_P", "0.8")]\n'
' if "--repeat-penalty" in help_txt:\n'
' args += ["--repeat-penalty", os.environ.get("GEN_REP_PEN", "1.05")]\n'
' if "--min-p" in help_txt:\n'
' args += ["--min-p", os.environ.get("GEN_MIN_P", "0.0")]\n'
' if "--metrics" in help_txt:\n args += ["--metrics"]',
)
# --------------------------------------------------------------------------
# 5b. LLAMA_ARG_TOP_K: esta e a UNICA var de sampling que o build aceita,
# entao vai por env (verificado presente no binario).
patch(
"LLAMA_ARG_TOP_K via env",
'def server_env():\n env = dict(os.environ)',
'def server_env():\n'
' env = dict(os.environ)\n'
' # Unica var de sampling confirmada no build b10985. As de temperatura\n'
' # e top_p NAO existem -- ver patch 5 (vao por flag).\n'
' env.setdefault("LLAMA_ARG_TOP_K",\n'
' os.environ.get("GEN_TOP_K", "20"))\n',
)
# --------------------------------------------------------------------------
# 6. Concorrencia: PARALLEL=1 medido em /props (total_slots: 1).
# Um slot unico serializa tudo: num loop de agente cada tool call espera a
# anterior, e um segundo cliente fica na fila atras do primeiro.
#
# ATENCAO -- isto e deliberadamente 1 por padrao, NAO aumentado.
# Motivo: com PARALLEL=N o llama.cpp divide o --ctx-size entre os slots.
# Com CTX_SIZE=32768 e N=2, cada slot fica com 16k, o que QUEBRA casos de
# contexto longo (ja foi observado n_tokens_max=14464 neste Space).
# Subir PARALLEL exige subir CTX_SIZE junto. Por isso fica explicito e
# documentado, mas o default nao muda sozinho.
patch(
"document PARALLEL/ctx tradeoff",
'PARALLEL = int(os.environ.get("PARALLEL", "1"))',
'PARALLEL = int(os.environ.get("PARALLEL", "1"))\n'
'# Medido: total_slots=1. Um slot unico serializa agentes concorrentes.\n'
'# Subir PARALLEL divide CTX_SIZE entre os slots -- para manter 32k por\n'
'# slot com N=Parallel, defina CTX_SIZE = 32768 * N. Ver README.\n'
'# print, nao log(): log() ainda nao existe neste ponto do arquivo.\n'
'if PARALLEL > 1 and CTX_SIZE < 32768 * PARALLEL:\n'
' print(f"[cfg] AVISO: PARALLEL={PARALLEL} com CTX_SIZE={CTX_SIZE} "\n'
' f"da apenas {CTX_SIZE // PARALLEL} tokens por slot "\n'
' f"(era 32768). Use CTX_SIZE={32768 * PARALLEL} para manter.",\n'
' flush=True)',
)
# --------------------------------------------------------------------------
# 7. Cache de prompt em RAM.
# Default do llama.cpp: --cache-ram 8192 (8 GB). Verificado na doc oficial.
# Este host tem ~96 GB de RAM e o modelo ocupa 18.6 GB; o KV cache de 32k
# custa ~3.2 GB. Ou seja, sobram ~70 GB ociosos.
# Cache maior = mais prefixos de conversa mantidos em memoria. Num agente,
# que revisita o mesmo historico a cada turno, isso evita re-prefill.
# Nao custa nada em qualidade: e so RAM parada que passamos a usar.
patch(
"raise --cache-ram (default 8 GB -> 16 GB)",
' if "--metrics" in help_txt:\n args += ["--metrics"]',
' # Cache de prompt em RAM. Default do llama.cpp e 8192 MiB.\n'
' # Com ~96 GB de RAM e 18.6 GB de modelo, ha folga para manter\n'
' # muitos prefixos de conversa. CACHE_RAM=0 desliga, -1 = sem limite.\n'
' # 32768 MiB (era 16384): com CTX_SIZE=131072 o KV de UM contexto custa\n'
' # 12 GiB, entao 16 GB guardava praticamente uma conversa so. 32 GB\n'
' # guarda ~2-3 e ainda sobra RAM (modelo 17.3 + KV 12 + cache 32 = 61\n'
' # de 97 GB). Nao custa velocidade -- e RAM que estava parada.\n'
' cache_ram = os.environ.get("CACHE_RAM", "32768")\n'
' if "--cache-ram" in help_txt:\n'
' args += ["--cache-ram", cache_ram]\n'
' log("cache: --cache-ram " + cache_ram + " MiB")\n'
' if "--metrics" in help_txt:\n args += ["--metrics"]',
)
# --------------------------------------------------------------------------
# 8. (REMOVIDO) Speculative decoding por n-gram.
# Testado em producao e MEDIDO INERTE: com `--spec-type ngram-map-k4v` o
# servidor aceita a flag mas gera ZERO draft tokens
# (`llamacpp:spec_decode_num_draft_tokens_total` = 0), inclusive no caso
# ideal (prompt com o mesmo bloco de codigo repetido 12x e instrucao
# explicita de repetir o padrao 20 vezes). Decode ficou em 28.0 tok/s,
# identico a sem spec.
# O A/B do autotune confirma: prefill 89.22 -> 89.85 e decode 14.12 -> 14.16
# na config vencedora (dentro do ruido); nas outras configs piorou ~10%.
# Manter uma flag que nao faz nada e pior que nao ter: induz a acreditar
# que ha uma otimizacao ativa. Removido.
# --------------------------------------------------------------------------
# 9. Correcao do criterio de escolha do AUTOTUNE.
# BUG REAL observado em producao: o autotune escolheu tb=8 (prefill 63.16,
# TTFT 72.63s) em vez de tb=16 (prefill 89.85, TTFT 51.05s) -- ou seja,
# trocou um TTFT 21 SEGUNDOS PIOR por ~3 tok/s de decode.
#
# Causa: o portao de elegibilidade exigia decode >= 90% do melhor medido.
# best_decode = 17.20 (tb=8) -> corte = 15.48
# tb=16 fa=on tinha decode 14.16 -> REPROVADO e excluido da escolha,
# apesar de ter o melhor prefill de todos.
# Com decode variando ate ~27% entre execucoes identicas (medido: tb=8 deu
# 13.55 numa rodada e 17.20 na outra), um portao de 90% persegue ruido e
# pode excluir a melhor configuracao inteira.
#
# Correcao: baixar o portao para 60%. Ele existe so para nao escolher algo
# com decode catastrofico; 60% cumpre isso sem ser vitima do ruido.
# Para carga de agente, TTFT/prefill domina a experiencia -- 20s a mais de
# espera por turno pesa muito mais que 3 tok/s de decode.
patch(
"fix autotune eligibility gate (0.9 -> 0.6)",
' best_decode = max(r["decode_tok_s"] for r in rows)\n'
' elegiveis = [r for r in rows if r["decode_tok_s"] >= 0.9 * best_decode]',
' best_decode = max(r["decode_tok_s"] for r in rows)\n'
' # Portao 0.9 era vitima do ruido: decode varia ~27% entre rodadas\n'
' # identicas, e o portao chegou a excluir a config de MELHOR prefill\n'
' # (tb=16 fa=on, decode 14.16 < corte 15.48), fazendo o autotune\n'
' # escolher tb=8 com TTFT 21s pior. 0.6 ainda barra decode\n'
' # catastrofico sem perseguir ruido.\n'
' elegiveis = [r for r in rows if r["decode_tok_s"] >= 0.6 * best_decode]',
)
# --------------------------------------------------------------------------
# 10. Contexto: 32768 -> 40960 (o maximo NATIVO, sem perda de qualidade).
#
# Verificado no config.json do proprio modelo (Qwen/Qwen3-30B-A3B):
# "max_position_embeddings": 40960
# "rope_scaling": null
# Ou seja: as position embeddings vao ate 40960 e NAO ha rope scaling
# configurado. Entao 40960 e alcancavel sem YaRN e sem risco de qualidade.
# O servidor rodava em 32768 -- 25% abaixo do que o modelo entrega de graca.
#
# O Qwen avisa na model card: "32,768 nativamente e 131,072 com YaRN", e
# "se o contexto medio nao passar de 32,768, NAO recomendamos YaRN, pois
# pode degradar performance". Por isso 40960 e o default seguro: e o teto
# sem YaRN.
#
# Memoria (medido/calculado): KV f16 = 96 KiB por token
# 32768 -> 3.0 GiB | 40960 -> 3.75 GiB | 131072 -> 12 GiB
# Com 96 GB de RAM e 18.6 GB de modelo, memoria NAO e o limite.
# O limite e o PREFILL: a ~105 tok/s, 40960 tokens custam ~390s (6.5 min).
# O cache de prompt amortiza isso quando o prefixo se repete.
patch(
"raise CTX_SIZE to 131072 (native on Qwen3-Coder, no YaRN)",
'CTX_SIZE = int(os.environ.get("CTX_SIZE", "32768"))',
'# Qwen3-Coder-30B-A3B tem 262144 de contexto NATIVO (rope_scaling=null),\n'
'# entao 131072 nao precisa de YaRN e nao tem risco de qualidade.\n'
'#\n'
'# Por que 131072 e nao 262144 (o maximo nativo):\n'
'# CTX_SIZE e um TETO, nao um custo -- alocar 262144 nao deixa requisicoes\n'
'# curtas mais lentas (a atencao escala com o tamanho REAL da sequencia).\n'
'# O motivo de parar em 131072 e o PREFILL: medido ~91 tok/s com 4.6k\n'
'# tokens, e a taxa cai com o contexto. Em 131072 o prefill de um prompt\n'
'# cheio fica na casa de dezenas de minutos; em 262144, horas. 128K cobre\n'
'# repositorio inteiro e ja e um teto realista. Subir para 262144 e uma\n'
'# Variable (CTX_SIZE=262144) -- 24 GiB de KV, cabe nos 97 GB.\n'
'CTX_SIZE = int(os.environ.get("CTX_SIZE", "131072"))',
)
# --------------------------------------------------------------------------
# 12. Profile do Qwen3-Coder-30B-A3B-Instruct.
# O app trazia 5 profiles, e NENHUM deles e o modelo certo para este hardware.
# Pior: dois sao densos de 32B (qwen3-32b, qwen25-coder-32b), que neste
# container rodam a ~2.6 tok/s (dense le TODOS os pesos por token) -- ou seja,
# sao inutilizaveis, apesar do nome "coder".
#
# Qwen3-Coder-30B-A3B: 30.5B total / 3.3B ATIVOS, 48 camadas, 4 KV heads.
# Arquitetura identica ao qwen3-30b-a3b -> mesma velocidade (~25 tok/s) e
# mesmo KV (96 KiB/token), mas:
# - especializado em coding agentico (SWE-bench 51.6, Terminal-Bench 31.3)
# - 262144 de contexto NATIVO (vs 40960) -- 6.4x, sem YaRN
# - Q4_K_M = 17.28 GiB, MENOR que o modelo anterior
# Revision fixada (como os outros profiles) para o build ser reproduzivel.
patch(
"add qwen3-coder-30b-a3b profile",
' "alias": "qwen2.5-coder-7b-agent",\n },\n}',
' "alias": "qwen2.5-coder-7b-agent",\n'
' },\n'
' # MoE 30.5B total / 3.3B ativos -- mesma arquitetura do qwen3-30b-a3b\n'
' # (48 camadas, 4 KV heads), entao mesma velocidade e mesmo KV, mas\n'
' # especializado em coding agentico e com 262144 de contexto nativo.\n'
' "qwen3-coder-30b-a3b": {\n'
' "repo": "unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF",\n'
' "revision": "b17cb02dd882d5b6ab62fc777ad2995f19668350",\n'
' "file": "Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf",\n'
' "size": 18556689568,\n'
' "alias": "qwen3-coder-30b-a3b-agent",\n'
' },\n'
'}',
)
# --------------------------------------------------------------------------
# 13. Default passa a ser o Coder.
# O profile existe (patch 12), mas o default ainda apontava para o modelo
# antigo. Trocar o default (em vez de depender de uma Variable) garante que
# o Space suba no modelo certo mesmo sem configuracao extra.
patch(
"default PROFILE -> qwen3-coder-30b-a3b",
'PROFILE = os.environ.get("PROFILE", "qwen3-30b-a3b")',
'PROFILE = os.environ.get("PROFILE", "qwen3-coder-30b-a3b")',
)
# --------------------------------------------------------------------------
# 11. YaRN opt-in para passar de 40960 (ate 131072).
# DESLIGADO por padrao, de proposito. O Qwen recomenda nao usar YaRN a menos
# que o contexto medio realmente passe de 32768, porque ele pode degradar a
# qualidade -- e num endpoint de CODIGO qualidade e o que importa.
#
# Para habilitar 131072 (4x de 32768, o maximo documentado pelo Qwen):
# ROPE_SCALE=4 (e CTX_SIZE=131072)
# Para 65536 (2x): ROPE_SCALE=2
patch(
"optional YaRN for >40960 context",
' if "--metrics" in help_txt:\n args += ["--metrics"]',
' # YaRN opt-in. Sem ROPE_SCALE definido, nada e passado -- o default\n'
' # fica no contexto nativo (40960) sem rope scaling.\n'
' rope_scale = os.environ.get("ROPE_SCALE", "")\n'
' if rope_scale and "--rope-scaling" in help_txt:\n'
' rope_type = os.environ.get("ROPE_SCALING", "yarn")\n'
' args += ["--rope-scaling", rope_type, "--rope-scale", rope_scale]\n'
' orig = os.environ.get("YARN_ORIG_CTX", "32768")\n'
' if "--yarn-orig-ctx" in help_txt:\n'
' args += ["--yarn-orig-ctx", orig]\n'
' log("rope: " + rope_type + " scale=" + rope_scale +\n'
' " orig_ctx=" + orig + " -- Qwen avisa que YaRN pode degradar")\n'
' if "--metrics" in help_txt:\n args += ["--metrics"]',
)
# --------------------------------------------------------------------------
# 14. Diagnostico de CPU: AMX e VNNI.
# O app checava so avx/avx2/fma/avx512f/avx512bw -- e ficava cego justamente
# na feature que mais importa para inferencia: **AMX**.
#
# Por que AMX importa: o build traz 14 backends de CPU compilados
# (libggml-cpu-*.so), incluindo `sapphirerapids` e `cooperlake`, que usam
# AMX (Advanced Matrix Extensions) para matmul INT8/BF16. Prefill e
# dominado por matmul, entao AMX e exatamente o eixo do gargalo.
# O Xeon Platinum 8559C e Emerald Rapids -- mesma familia de nucleo do
# Sapphire Rapids, que introduziu AMX. Se AMX estiver disponivel e o
# backend certo for carregado, o prefill ganha sem custo nenhum.
#
# `/proc/cpuinfo` expoe: amx_tile, amx_int8, amx_bf16, avx512_vnni,
# avx512_bf16. Reportar isso torna o ganho verificavel em vez de suposto.
patch(
"report AMX / VNNI / bf16 cpu flags",
' for f in ("avx", "avx2", "fma", "avx512f", "avx512bw"):',
' # AMX e VNNI sao o que decide a velocidade de PREFILL (matmul).\n'
' for f in ("avx", "avx2", "fma", "avx512f", "avx512bw",\n'
' "avx512_vnni", "avx512_bf16",\n'
' "amx_tile", "amx_int8", "amx_bf16"):',
)
# --------------------------------------------------------------------------
# 15. Logar o backend de CPU E o uso real de AMX.
#
# Pesquisa nos mantenedores do llama.cpp (issue #12003) revelou o que checar:
# - "amx works better for PREFILL (M is large)"; decode usa AVX512-VNNI
# de proposito, porque VNNI ganha quando M=1.
# - "The moe modules in llama.cpp needs different optimizations (group
# gemm), it is not a matter of using amx or not" -> para MoE o ganho de
# AMX e LIMITADO por limitacao do framework, nao da CPU.
# - O llama.cpp imprime QUANTO do modelo foi para o buffer AMX:
# "load_tensors: AMX model buffer size = X MiB"
# "load_tensors: tensor '...' (q4_K) cannot be used with ... AMX"
# Ou seja: da para SABER se AMX esta ativo e quanto, em vez de supor.
# - "system_info:" lista as flags como o llama.cpp as ve (AMX_INT8 = 1 ...).
#
# A versao anterior procurava so "ggml-cpu"/"load_backend" e nao pegava nada,
# porque as linhas uteis sao "load_tensors:" e "system_info:".
patch(
"log ggml cpu backend + real AMX usage",
' for line in txt.splitlines():\n'
' if "Chat format" in line or "chat_template" in line.lower():\n'
' log("diag> " + line.strip()[:200])',
' for line in txt.splitlines():\n'
' if "Chat format" in line or "chat_template" in line.lower():\n'
' log("diag> " + line.strip()[:200])\n'
' # backend carregado e, mais importante, quanto do modelo foi para\n'
' # o buffer AMX (linha "load_tensors: ... AMX").\n'
' low = line.lower()\n'
' if any(k in low for k in ("load_backend", "ggml-cpu",\n'
' "system_info", "load_tensors",\n'
' "amx", "buffer type")):\n'
' log("diag> " + line.strip()[:200])',
)
# --------------------------------------------------------------------------
# 16. Reportar topologia NUMA.
# O host tem 192 nucleos logicos (provavelmente 2 sockets). Se o container
# for espalhado por mais de um no NUMA, o acesso a memoria pode atravessar
# o interconect entre sockets -- o que pesa justamente em inferencia, que e
# limitada por banda. Saber quantos nos existem e se ha memoria por no e o
# que decide se vale usar --numa / --cpu-mask.
# Se houver 1 no so, NAO ha o que otimizar e paramos por aqui.
patch(
"report NUMA topology",
' if TEMPLATE_FILE.is_file():',
' # NUMA: so importa se houver mais de um no visivel ao container.\n'
' try:\n'
' import glob as _glob\n'
' nodes = sorted(_glob.glob("/sys/devices/system/node/node*"))\n'
' log("numa: " + str(len(nodes)) + " no(s) visivel(is)")\n'
' if len(nodes) > 1:\n'
' for n in nodes:\n'
' try:\n'
' cpul = pathlib.Path(n, "cpulist").read_text().strip()\n'
' log("numa> " + n.rsplit("/", 1)[-1] + " cpus=" + cpul)\n'
' except Exception:\n'
' pass\n'
' except Exception as exc:\n'
' log(f"numa: {exc}")\n'
' if TEMPLATE_FILE.is_file():',
)
# --------------------------------------------------------------------------
# 17. Modo NUMA (opt-in, medido antes de virar default).
# Medido no container: 2 nos NUMA (node0 cpus=0-47,96-143 | node1 48-95,144-191).
# Num host de 2 sockets, memoria alocada num no e lida pelo outro atravessa o
# interconect -- e inferencia e limitada por banda. O llama.cpp tem --numa
# justamente para isso (distribute = interleave a memoria entre os nos).
#
# Porem NAO e garantido que ajude: o container recebe 16 vCPU de 192 e pode
# ja estar restrito a um subconjunto de nos. Por isso entra como opt-in via
# env (NUMA_MODE), com default VAZIO = nao passa a flag = comportamento de
# antes. Depois de medir, decidimos se vira default.
# NUMA_MODE=distribute -> interleave (esperado melhor em banda)
# NUMA_MODE=isolate -> cada thread no seu no
patch(
"optional --numa mode",
' if "--metrics" in help_txt:\n args += ["--metrics"]',
' # NUMA opt-in: sem NUMA_MODE definido, nada muda em relacao a antes.\n'
' numa_mode = os.environ.get("NUMA_MODE", "")\n'
' if numa_mode and "--numa" in help_txt:\n'
' args += ["--numa", numa_mode]\n'
' log("numa: --numa " + numa_mode + " (opt-in)")\n'
' if "--metrics" in help_txt:\n args += ["--metrics"]',
)
# --------------------------------------------------------------------------
# 18. ubatch 2048 -> 4096 (experimento focado em PREFILL).
# O autotune media threads/threads_batch, mas mantinha ubatch FIXO em 2048 --
# entao esse eixo nunca foi realmente testado.
# Por que ubatch importa aqui: prefill processa o prompt em lotes de ubatch
# tokens. Lote maior = matmul maior = melhor eficiencia. E os mantenedores
# do llama.cpp apontaram que "the moe modules needs different optimizations
# (e.g. group gemm)" -- group gemm justamente melhora com lote maior, porque
# agrupa as matmuls dos especialistas ativados. Ou seja: ubatch e o knob que
# ataca a limitacao conhecida de MoE em CPU.
# 4096 ainda cabe folgado: ativacoes ~4096 x 2048 x 2B = 16 MB por tensor.
# Continua sobrescrevivel por env (UBATCH_SIZE), entao da para A/B sem deploy.
patch(
"ubatch: mantido 2048 (experimento revertido)",
'UBATCH_SIZE = int(os.environ.get("UBATCH_SIZE", "2048"))',
'# MANTIDO 2048. Testei 4096 e NAO ajudou -- ver bloco 18 acima.\n'
'# Fica explicito para ninguem "otimizar" de novo sem medir.\n'
'UBATCH_SIZE = int(os.environ.get("UBATCH_SIZE", "2048"))',
)
# --------------------------------------------------------------------------
# 19. Coerencia batch >= ubatch.
# BUG que eu mesmo introduzi no patch 18: subi UBATCH_SIZE para 4096 mas
# BATCH_SIZE continuou 2048. No llama.cpp o lote fisico (ubatch) NAO pode ser
# maior que o logico (batch) -- ele faz clamp de n_ubatch para n_batch. Ou
# seja: a mudanca podia estar sendo silenciosamente anulada, e a "melhora"
# medida vinha de diferenca de metodologia, nao da flag.
# Correcao dupla: (a) subir BATCH_SIZE para 4096, (b) guarda que forca
# batch >= ubatch e AVISA no log se alguem configurar ao contrario.
# O aviso e o ponto: configuracao incoerente tem de aparecer, nao ser
# engolida em silencio.
patch(
"guard: force batch >= ubatch and warn",
'UBATCH_SIZE = int(os.environ.get("UBATCH_SIZE", "2048"))',
'UBATCH_SIZE = int(os.environ.get("UBATCH_SIZE", "2048"))\n'
'# Guarda de coerencia: sem isso um UBATCH_SIZE maior que BATCH_SIZE e\n'
'# anulado em silencio pelo llama.cpp e a flag vira placebo.\n'
'if UBATCH_SIZE > BATCH_SIZE:\n'
' print(f"[cfg] UBATCH_SIZE={UBATCH_SIZE} > BATCH_SIZE={BATCH_SIZE}; "\n'
' f"subindo BATCH_SIZE para {UBATCH_SIZE} (llama.cpp faria clamp)",\n'
' flush=True)\n'
' BATCH_SIZE = UBATCH_SIZE',
)
def apply(path, dry_run=False):
src = pathlib.Path(path).read_text(encoding="utf-8")
original = src
report = []
for name, old, new in PATCHES:
# Idempotencia: se TODAS as linhas novas ja estao no arquivo, o patch
# ja foi aplicado. Comparar `new in src` nao serve quando o patch
# consiste em INSERIR um bloco antes de uma ancora que ja existe --
# nesse caso `new` nunca aparece literalmente como substring, porque
# o bloco inserido e a ancora ficam separados por linhas ja presentes.
# Por isso o teste e por linhas: as linhas adicionadas tem de existir.
added = [ln for ln in new.splitlines() if ln.strip()
and ln not in old.splitlines()]
if added and all(a in src for a in added):
report.append(f"[SKIP] {name}: ja aplicado")
continue
occurrences = src.count(old)
if occurrences == 0:
report.append(f"[FALHA] {name}: ancora nao encontrada")
print("\n".join(report))
return 1, None
if occurrences > 1:
report.append(f"[FALHA] {name}: ancora ambigua ({occurrences}x)")
print("\n".join(report))
return 1, None
src = src.replace(old, new, 1)
report.append(f"[OK] {name}")
if src == original:
report.append("nada a fazer (todos os patches ja aplicados)")
if dry_run:
diff = difflib.unified_diff(
original.splitlines(keepends=True),
src.splitlines(keepends=True),
fromfile="app.py (atual)", tofile="app.py (patched)", n=3,
)
print("\n".join(report))
print("\n".join(diff))
return 0, None
# newline="\n" explicito: sem isso o write_text traduz para CRLF no
# Windows, e o arquivo local fica com sha256 diferente do que foi
# publicado (o upload converte para LF). O conteudo e o mesmo, mas
# comparar hash local-vs-remoto vira falso alarme. Escrevendo LF desde
# aqui, local e publicado batem byte a byte.
pathlib.Path(path).write_text(src, encoding="utf-8", newline="\n")
print("\n".join(report))
return 0, src
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__)
sys.exit(2)
target = sys.argv[1]
dry = "--dry-run" in sys.argv
code, _ = apply(target, dry_run=dry)
sys.exit(code)
|