mirror of
https://github.com/mcloh/decision-models-benchmark.git
synced 2026-11-05 22:04:12 +00:00
- config/calibration.json: temperatura e limiar de abstenção por candidato e dispositivo, ajustados só na partição de calibração. - config/frozen.json: hashes de configuração, calibração, partições, adaptadores, contrato e ambientes; libera a partição de teste. - Q6: lote de produção = 1; tarefa 65 não se aplica neste ciclo (D4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
13 KiB
Markdown
164 lines
13 KiB
Markdown
# 04 — Decisões de projeto
|
||
|
||
As decisões do benchmark seguem um mesmo critério: preferir a **abordagem automatizada e verificável**. Isso significa regras determinísticas em código, gates que fazem o pipeline falhar, valores calculados a partir de medições e parâmetros pré-registrados em [config/benchmark.yaml](../config/benchmark.yaml). Nenhuma decisão é tomada caso a caso durante a execução, e os valores pré-registrados são congelados com hash na tarefa 62.
|
||
|
||
## Resumo
|
||
|
||
| # | Tema | Decisão | Mecanismo |
|
||
|---|------|---------|-----------|
|
||
| Q1 | Contrato | `POST /v1/systemone` público (`state` + `questions` tipadas → `answers` + `usage`), congelado como `jev-compat-v1` | JSON Schema em `contracts/jev/schemas/` e testes de conformidade |
|
||
| Q2 | Número de opções | Até **16** por pergunta: 15 de domínio + `sem_correspondencia` | Validação de esquema e lint do conjunto de dados |
|
||
| Q3 | Isolamento de rede | Aquisição fora da rede dos jobs; medições sem acesso a fontes externas | Gate de egresso, bloqueio no processo e verificação por SHA-256 |
|
||
| Q4 | Interfaces e licenças | Uma interface por candidato (abaixo); só licenças Apache-2.0 ou MIT | A aquisição lê a licença no Hub e falha fora da lista permitida |
|
||
| Q5 | Critérios de sucesso | Limite de latência fixo e não inferioridade em relação ao baseline | Avaliação automática a partir de `config/benchmark.yaml` |
|
||
| Q6 | Lote de produção | Lote 1 (contrato por requisição, decisão turno a turno) | Vazão medida com lote 1 |
|
||
| Q7 | Dados e anotação | 100% sintéticos; anotação por três modelos generativos em rodízio | `datagen/` ([02 §4](02-desenho-experimental.md#4-dados)) |
|
||
| Q8 | Contexto no `state` | Orçamento comum de tokens; D1 usa só o enunciado atual | `dmb.canonical.fit_state` |
|
||
| Q9 | Baseline LLM | Incluído como referência, fora do ranking principal | Adaptador no mesmo contrato |
|
||
| Q10 | Calibração | Temperature scaling por candidato e tarefa, só na partição de calibração | Ajuste por NLL e congelamento com hash |
|
||
| Q11 | CPU | Medida com timeout; inviabilidade declarada por regra | p95 > timeout → `inviavel_cpu` |
|
||
| Q12 | Tamanho do teste | ≥ 1.300 decisões D1 (análise de poder) | Recalculado na análise |
|
||
| Q13 | Custos | Sem orçamento provisionado neste ciclo; custo estimado no relatório | Estimativa por cenário (tarefa 70) |
|
||
| Q14 | Cenário de GPU | `VM.GPU.A10.1` em sa-saopaulo-1, execuções em fila | `infra/ds/gpu_queue.py` e réplica dos artefatos |
|
||
|
||
---
|
||
|
||
## Q1 — Contrato `jev-compat-v1`
|
||
|
||
Base: a forma pública do `POST /v1/systemone`, a mesma reproduzida pelas implementações abertas (OpenJev/SemIf).
|
||
|
||
**Requisição**
|
||
|
||
```json
|
||
{
|
||
"model": "<model_id>",
|
||
"state": "<texto> | <objeto JSON>",
|
||
"questions": {
|
||
"<nome>": {"type": "choice", "instructions": "...", "criteria": {"<opcao>": "<descrição>"}},
|
||
"<nome>": {"type": "noul", "instructions": "...", "criteria": {"true": "...", "false": "..."}},
|
||
"<nome>": {"type": "score", "instructions": "...", "criteria": ["<nível 0>", "<nível 1>", "..."]}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Resposta**
|
||
|
||
```json
|
||
{
|
||
"model": "<model_id>",
|
||
"answers": {
|
||
"<nome>": {"type": "choice", "choice": "<opcao>", "confidence": 0.0, "probabilities": {"<opcao>": 0.0}},
|
||
"<nome>": {"type": "noul", "noul": 0.0},
|
||
"<nome>": {"type": "score", "score": 0.0, "confidence": 0.0,
|
||
"probabilities": {"0": 0.0}, "legend": {"0": "<nível 0>"}}
|
||
},
|
||
"usage": {"input_tokens": 0, "output_tokens": 0},
|
||
"metadata": {"latency_ms": 0.0, "model_id": "...", "run_id": "...", "adapter_version": "...",
|
||
"diagnostics": {}}
|
||
}
|
||
```
|
||
|
||
**Comportamentos padronizados (tarefa 40)**
|
||
|
||
| Situação | Código | Contabilização |
|
||
|----------|--------|----------------|
|
||
| Requisição inválida | `422` | Erro do harness: o job falha |
|
||
| Estado acima do contexto, depois do truncamento comum | `422` | Erro de decisão (`error_type=context`) |
|
||
| Timeout | `504` | Erro de decisão (`error_type=timeout`) |
|
||
| Opção devolvida fora de `criteria` ou com a ordem alterada | `502` | Erro de decisão (`error_type=unknown_option`) |
|
||
| Modelo indisponível | `503` | Erro de execução |
|
||
|
||
**Mapeamento dos tipos (tarefas 38–39)**
|
||
|
||
- `choice` → D1, D2 e D5. A predição é `choice`; a distribuição é `probabilities`.
|
||
- `noul` → D3 e as perguntas por intenção de D4. P(sim) = `noul`.
|
||
- `score` → reservado para ciclos futuros.
|
||
- **`confidence` não é calibrada** em nenhum candidato. As métricas de calibração e abstenção usam `max(probabilities)` depois da calibração da tarefa 61; a `confidence` nativa é só registrada.
|
||
- Todas as distribuições recebem um piso numérico comum (1e-6) e são renormalizadas, preservando a ordem das opções.
|
||
- `output_tokens` é sempre 0, porque não há geração.
|
||
- `metadata.diagnostics` traz avisos do próprio modelo, por exemplo opções truncadas pelo orçamento interno do Laya.
|
||
|
||
## Q2 — Número de opções
|
||
|
||
O limite comum é de **16 opções**, incluindo `sem_correspondencia`. Esse é o máximo aceito pela interface de todos os candidatos: o SemIf usa as letras A–P, o Laya recomenda até cerca de 20 e o Rizzo aceita 26. Um exemplo com mais de 16 opções reprova o lint e impede o congelamento.
|
||
|
||
## Q3 — Isolamento de rede
|
||
|
||
- **Aquisição.** Pesos, tokenizadores, código-fonte e runtimes são obtidos fora da rede dos jobs e publicados no bucket de artefatos, com SHA-256 no manifesto. Os ambientes dos candidatos são montados por jobs de build e também publicados no bucket.
|
||
- **Medições em CPU.** Rodam numa sub-rede privada que só alcança os serviços OCI pela Service Gateway. O gate de egresso tenta abrir conexão com hosts públicos (Hugging Face Hub, PyPI, GitHub) e só aprova se todas as tentativas falharem.
|
||
- **Medições em GPU.** Rodam na rede gerenciada do serviço, com o isolamento garantido no processo (ver Q14).
|
||
- **Em todas as medições:** variáveis offline, bloqueio de sockets fora do loopback e um autoteste registrado em cada resultado (`process_guard`).
|
||
- `infra/oci/nat.py` permite abrir uma saída temporária por NAT, caso alguma aquisição futura precise ser feita de dentro da VCN.
|
||
|
||
## Q4 — Interfaces e licenças dos candidatos
|
||
|
||
| Candidato | Licença | Interface usada pelo adaptador | Contexto | Observação |
|
||
|-----------|---------|--------------------------------|----------|------------|
|
||
| Laya multilingual | Apache-2.0 | `laya.load(...).predict(state, questions)`, nativo no formato JEV | 1.024 tokens (até 8.192) | Pergunta e opções dividem um orçamento interno de cabeçalho (`head_max_len`). As probabilidades vêm descalibradas e arredondadas em 4 casas |
|
||
| SemIf + Qwen3.5-4B | MIT (SemIf), Apache-2.0 (Qwen) | Modo `direct`: um forward pass lê os logits das opções declaradas | 8.192 tokens | Revisão do Qwen fixada no manifesto; bf16 |
|
||
| GLiNER2.5-multi-Decide | Apache-2.0 | `classify_text` com rótulos descritos; distribuição completa pelo softmax do próprio modelo (`multi_label` com `class_act="softmax"` e limiar 0) | 4.096 tokens | A pergunta entra como prefixo do texto, porque `classify_text` não tem campo de instrução |
|
||
| Rizzo Flow 4B | Apache-2.0 (base); dados de treino com componentes CC-BY-SA/CC-BY | Prompts `spark-decisions-v3` e leitura dos logits das letras de resposta, GGUF Q8_0 no llama.cpp | 2.048 tokens | Formato de prompt obrigatório; documentação só em inglês |
|
||
|
||
- A **lista de licenças permitidas** fica em `config/benchmark.yaml`. A aquisição lê `cardData.license` no Hub e falha fora da lista.
|
||
- O **contexto efetivo** de cada candidato fica registrado no adaptador e no resumo de cada execução.
|
||
|
||
## Q5 — Critérios de sucesso pré-registrados
|
||
|
||
- **Desempenho (H3):** em GPU com lote 1, p95 ≤ **150 ms por decisão**. Em CPU, os valores são reportados como viabilidade.
|
||
- **Acurácia (H1):** **não inferioridade em relação ao baseline LLM**, com margem de **3 p.p.**: o limite inferior do IC 95% (bootstrap pareado por grupo) da diferença precisa ser > −3 p.p. Sem baseline, vale o critério absoluto de macro F1 ≥ 0,80.
|
||
- **Risco (H2):** no limiar escolhido na calibração, a taxa de **rota errada com confiança alta** precisa ser ≤ **2%**, com cobertura ≥ **80%**.
|
||
|
||
## Q6 — Lote de produção
|
||
|
||
O contrato `POST /v1/systemone` é por requisição, e o orquestrador decide turno a turno, no caminho síncrono do usuário. Os adaptadores atendem uma requisição por vez, então **o lote de produção é 1** e a vazão por instância é medida com lote 1 (decisões por segundo = 1000 / latência média). Ganhos de vazão com lotes maiores ou com réplicas concorrentes ficam como trabalho futuro e não entram no ranking deste ciclo.
|
||
|
||
## Q7 — Dados sintéticos e anotação por modelos generativos
|
||
|
||
- **Dados 100% sintéticos** no domínio de atendimento de telefonia móvel, incluindo pedidos fora de escopo e enunciados emocionais. São gerados por modelos de fornecedores diferentes dos anotadores (`meta.llama-4-maverick` e `cohere.command-a`).
|
||
- **Preparação:** mascaramento de dados pessoais (CPF, CNPJ, cartão, telefone, e-mail, CEP), deduplicação exata e aproximada, e limites de comprimento.
|
||
- **Anotação:** três modelos generativos de fornecedores distintos (`openai.gpt-5.5`, `google.gemini-2.5-pro` e `xai.grok-4.3`), em rodízio. Por lote, dois anotam de forma independente, sem ver o rótulo de geração, e o terceiro desempata. Três rótulos distintos excluem o exemplo.
|
||
- **Partição** por lote de geração (`group_id`), para que paráfrases do mesmo lote nunca fiquem em partições diferentes.
|
||
- **Limitação:** os rótulos de referência refletem o consenso de modelos generativos, não de pessoas. O relatório traz a concordância entre anotadores (kappa por par) e com o rótulo de geração.
|
||
|
||
## Q8 — Contexto no `state` e orçamento de tokens
|
||
|
||
- Todos os candidatos recebem o mesmo texto serializado (`state-text-v1`), com orçamento de **512 tokens** para o estado.
|
||
- A regra de truncamento remove primeiro os turnos mais antigos e só corta o fim do enunciado se ele sozinho estourar o orçamento. O exemplo recebe `truncated=true`.
|
||
- Em D1 não há histórico (`n_turns = 0`). O maior estado do conjunto d1-v1 tem menos de 120 tokens em qualquer tokenizador, então não há truncamento.
|
||
- Para tarefas com histórico (D2–D4), N ∈ {0, 1, 2, 4} é definido como o maior valor em que ≥ 95% dos exemplos de desenvolvimento cabem no orçamento.
|
||
|
||
## Q9 — Baseline LLM
|
||
|
||
- Um LLM generalista do OCI Generative AI, com a versão fixada no manifesto e adaptador no mesmo contrato, com saída restrita às opções.
|
||
- Quando há logprobs, as probabilidades vêm deles. Sem logprobs, a distribuição é one-hot e o baseline fica fora das métricas de calibração.
|
||
- Usa a mesma serialização e os mesmos dados. Serve de referência para H1 e para custo, fora do ranking principal.
|
||
|
||
## Q10 — Calibração
|
||
|
||
- **Temperature scaling** por candidato e tarefa: um único parâmetro, ajustado por minimização de NLL na partição de calibração.
|
||
- O calibrador só é aceito se reduzir o ECE em validação cruzada de 5 dobras por `group_id`. Caso contrário, mantém-se T = 1.
|
||
- Os limiares de abstenção são escolhidos sobre as probabilidades calibradas, maximizando a cobertura com taxa de rota errada com confiança alta ≤ 2%.
|
||
- Os parâmetros T e os limiares são versionados e congelados com hash na tarefa 62.
|
||
|
||
## Q11 — CPU
|
||
|
||
- Timeout de **10 s por decisão**.
|
||
- Um candidato com p95 acima do timeout, ou com taxa de timeout acima de 5%, recebe `inviavel_cpu` e fica fora das rodadas finais em CPU. Os resultados em CPU são reportados como viabilidade, não como ranking.
|
||
|
||
## Q12 — Tamanho do conjunto de teste final
|
||
|
||
- Análise de poder para a comparação pareada (McNemar) entre dois candidatos em D1: diferença mínima detectável de 3 p.p., α = 0,05, poder de 0,80, discordância esperada de 0,15. Isso dá n ≈ **1.300 decisões**.
|
||
- O teste d1-v1 tem **1.744 decisões**.
|
||
- A análise recalcula o efeito mínimo detectável com a discordância observada e com a correção de agrupamento por `group_id`.
|
||
|
||
## Q13 — Custos
|
||
|
||
O uso deste ciclo está coberto por cota interna de engenharia, então não há orçamento nem alertas provisionados. O relatório traz o **custo estimado por mil decisões** em cada cenário (tarefa 70), calculado a partir do preço público do shape e da vazão medida.
|
||
|
||
## Q14 — Cenário de GPU
|
||
|
||
- Todas as medições de GPU usam o mesmo shape e a mesma região: `VM.GPU.A10.1` em **sa-saopaulo-1**, com execuções em fila (`infra/ds/gpu_queue.py`).
|
||
- Pesos e ambientes são replicados do lado do servidor para `dmb-artefatos-gru` (`infra/oci/replicate.py`); os resultados ficam em `dmb-resultados-gru`.
|
||
- Os jobs usam a rede gerenciada do serviço, e o isolamento é feito no processo (`DMB_ISOLATION=app`): variáveis offline, bloqueio de sockets e autoteste (`process_guard`) registrado em cada resultado.
|
||
- As medições em CPU usam a sub-rede privada em us-chicago-1. CPU e GPU são reportadas em separado.
|