Spaces:
Paused
Paused
revert: ubatch volta a 2048 (4096 mediu ~3% PIOR em A/B controlado); mantem guarda batch>=ubatch
89b8453 verified Download patch_app.py from Erinaldordso1996/Udud: direct link, hf CLI and curl.
- Browser
- Download file 29.4 kB
-
https://huggingface.co/spaces/Erinaldordso1996/Udud/resolve/main/patch_app.py
- Command line
-
hf download hf://spaces/Erinaldordso1996/Udud/patch_app.py
-
curl -L -o patch_app.py https://huggingface.co/spaces/Erinaldordso1996/Udud/resolve/main/patch_app.py
29.4 kB
| """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) | |