Harness de avaliação
laya.evals transforma um conjunto de dados rotulado em uma pontuação repetível, e uma linha de base
em um gate de passa/falha, para que uma mudança de qualidade seja um diff revisável em vez de uma
checagem manual.
A matemática das métricas e o parser do conjunto de dados são Python puro mais numpy e nunca importam o torch, então rodam sem pesos. Rodar um conjunto de dados contra um checkpoint precisa do checkpoint e leva seu tempo normal de carga.
Início rápido
# check the format without a model
laya-evals validate research/evals/fixture.jsonl
# score a labelled set on one checkpoint, with thresholds and a baseline
laya-evals run data.jsonl --model english --device cpu \
--min-accuracy 0.8 --max-ece 0.05 --score-within 0.25 --slice language \
--json report.json --markdown report.md
# compare a saved report to a baseline
laya-evals compare report.json --baseline baseline.json --tolerance choice_accuracy=0.02
laya eval ... é a mesma coisa através da CLI principal, então laya eval validate data.jsonl
também funciona.
Códigos de saída: 0 em sucesso, 1 quando um limiar ou uma tolerância de linha de base falha, 2
em erro de uso. run imprime as métricas gerais e quaisquer fatias pedidas na saída padrão, e grava
o relatório completo e um resumo em Markdown quando --json / --markdown são informados.
Atribuir erros de shortlist
Para um conjunto de escolhas rotulado de alta cardinalidade, laya.evals_shortlist.evaluate_shortlist
usa o caminho predict_shortlist existente e o harness de avaliação regular. Ele responde a duas
perguntas separadas: a recuperação manteve o rótulo ouro, e o Laya o escolheu quando ele estava
presente? Esta é uma API Python opt-in para rótulos de choice; relatórios comuns de
laya-evals run não mudam.
import laya
from laya.evals import Dataset
from laya.evals_shortlist import evaluate_shortlist
from laya.shortlist import embed_fn_from_agent
agent = laya.load()
dataset_path = "intents.jsonl"
dataset = Dataset.from_jsonl(dataset_path)
report = evaluate_shortlist(
agent, dataset, embed_fn_from_agent(agent), k=20,
checkpoint_id="my-checkpoint@revision", embedder_id="my-encoder@revision",
dataset_path=dataset_path,
)
print(report.overall)
print(report.cases[0]["shortlist_status"])
Use a mesma função de embedding e o mesmo checkpoint da implantação que está sendo medida. Os dois
identificadores são fornecidos pelo chamador e devem nomear revisões imutáveis; o relatório não
consegue inferir os pesos por trás de um callable arbitrário. dataset_path registra o SHA256 do
arquivo ao lado da impressão digital de perguntas existente. Cada caso mantém os rótulos de shortlist
reais e um dentre correct, retrieval_miss ou decision_miss. shortlist_recall_at_k é a fração
de rótulos ouro retidos. shortlist_accuracy_on_recalled são as decisões corretas divididas pelos
casos retidos; é omitido quando nenhum foi retido. O choice_accuracy existente continua sendo a
acurácia de ponta a ponta sobre todos os casos, incluindo os erros de recuperação. As métricas de
shortlist aparecem nas mesmas fatias de idioma, modelo, pergunta e tag. A latência da solicitação
inclui o embedding e a chamada de decisão; o relatório não isola os tempos de cada etapa. Com
k >= n, a pergunta original passa direto e a recuperação tem recall 1 sem chamar o embedder.
Isto não reproduz os resultados do BANKING77 na issue #102: esses números dependem do conjunto de dados, do checkpoint e do bi-encoder dela. Esta API torna o mesmo tipo de diagnóstico repetível em um conjunto rotulado próprio do chamador.
Avaliar uma exportação ONNX
run --onnx PATH pontua um modelo ONNX exportado através do ONNXAgent em vez do Router do torch,
então uma implantação ONNX (incluindo uma cópia INT8 de
scripts/export_onnx.py --quantize) passa pelos mesmos limiares e linhas de base que o caminho do
torch:
python scripts/export_onnx.py --model convaiinnovations/laya --output laya.onnx --quantize
laya-evals run data.jsonl --onnx laya.int8.onnx --max-ece 0.05
--model nomeia o checkpoint de onde veio a exportação — um id do Hub ou caminho local, não um nome
curto de Router como english, já que não há Router nesse caminho (padrão convaiinnovations/laya).
Sua config e seu tokenizer são carregados de lá. O agente serve um único checkpoint, então uma linha
do conjunto de dados cujo campo model nomeia outro falha com um erro claro em vez de ser respondida
em silêncio pelo modelo errado; --device não se aplica. --batch-size usa a API de lote do agente
quando ele tem uma e cai para uma chamada por estado caso contrário; --sort-by-length é repassado
para essa API de lote; o fallback por estado não tem grupo para reordenar. Passe --calibration PATH
para carregar um mapa de calibração ajustado no ONNXAgent, de modo que gates de calibração como
--max-ece avaliem contra probabilidades calibradas. O bloco config do relatório registra o
caminho onnx e o caminho calibration (quando definido).
Medido em research/evals/fixture.jsonl (12 linhas rotuladas, checkpoint em inglês, CPU):
| runner | choice_acc | noul_acc | score_mae | ece | mean_conf | p50 ms |
|---|---|---|---|---|---|---|
| torch Router | 0.75 | 1.00 | 1.3418 | 0.1596 | 0.7304 | 116.8 |
--onnx fp32 |
0.75 | 1.00 | 1.3418 | 0.1596 | 0.7304 | 66.3 |
--onnx int8 |
0.75 | 1.00 | 1.3512 | 0.1658 | 0.7304 | 46.3 |
A exportação fp32 reproduz os números do torch exatamente, e a cópia quantizada move score_mae em
0.009 e ece em 0.006 — o tipo de desvio que compare --tolerance deve barrar.
Formato do conjunto de dados
Um objeto JSON por linha (JSONL). Linhas em branco e linhas que começam com # são ignoradas.
| campo | obrigatório | significado |
|---|---|---|
state |
sim | texto, e-mail, ticket ou documento JSON sobre o qual decidir |
questions |
sim | um dict de pergunta do Laya, exatamente como Router.predict aceita |
expected |
sim | verdade de referência indexada por id de pergunta: um rótulo para choice, um número para score, true/false para noul |
tags |
não | strings pelas quais fatiar |
language |
não | um código pelo qual fatiar |
model |
não | força um checkpoint para esta linha; --model o sobrescreve. Uma linha que não força nada é rotulada com o checkpoint com que o Router respondeu |
research/evals/dataset.template.jsonl tem um exemplo comentado.
Métricas
Cada métrica é calculada por resposta onde se aplica e agregada sobre o conjunto de dados:
| métrica | aplica-se a | significado |
|---|---|---|
choice_accuracy |
choice |
fração cujo rótulo escolhido corresponde |
noul_accuracy |
noul |
fração cujo booleano (probabilidade >= 0.5) corresponde |
score_mae |
score |
erro absoluto médio |
score_within_<tol> |
score |
fração dentro de uma tolerância absoluta |
ece |
qualquer resposta com confiança | erro de calibração esperado, 15 bins, calculado sobre answer["answer_confidence"], a probabilidade calibrada que o Laya reporta em todo tipo de resposta |
brier |
qualquer resposta com confiança e rótulo conhecido | Brier score da confiança como P(correct), mean((confidence - correct)**2); menor é melhor |
aurc |
qualquer resposta com confiança e rótulo conhecido | área sob a curva risco–cobertura: um valor de risco por nível de confiança distinto, cada um ponderado pelas respostas que aquele nível abrange; menor é melhor, e recompensa uma confiança que ordena certo e errado, não apenas calibrada |
selective_accuracy@50, selective_accuracy@80 |
qualquer resposta com confiança e rótulo conhecido | acurácia sobre as respostas que um limiar de confiança no ponto de cobertura de 50% / 80% aceita – o que abster-se da cauda menos confiante compra. Um limiar não consegue dividir um grupo de confianças iguais, então isso pode cobrir mais do que a fração nomeada; veja cortes de cobertura |
mean_confidence |
qualquer resposta com confiança | média do answer["answer_confidence"] reportado |
latency_p50_ms, latency_p95_ms |
por solicitação | tempo de parede que cada solicitação esperou, informativo – veja batching |
cost_per_decision_p50_ms, cost_per_decision_p95_ms |
por decisão | tempo de parede de uma chamada dividido pelas linhas que ela carregou, informativo |
Cortes de cobertura e empates
Ambas as métricas de cobertura cortam em um limiar de confiança, e um limiar aceita toda resposta
em sua própria confiança. Então um corte nunca divide um grupo de respostas que compartilham uma:
quando coverage * n cai dentro de tal grupo, todo membro do grupo é aceito. O número de respostas
por trás da cifra é, portanto, a borda superior do grupo, e não a fração nomeada –
selective_accuracy@50 sobre uma fatia cujas confianças são todas iguais é a própria acurácia dessa
fatia, não a melhor metade dela. A contagem que o gate imprime (n= na mensagem de falha de uma
regra) é o tamanho da fatia, não o tamanho aceito, então um grupo muito largo não é visível apenas
pela mensagem.
Empates são o caso normal, não um canto: uma temperatura ajustada pode deixar um bucket reportando
uma massa pontual, o que laya.common.answer_confidence registra do choice:11+ distribuído, e um
checkpoint real produziu um grupo de seis linhas exatamente em 1.0 de doze respostas. Cortar em um
índice de linha em vez disso fez ambas as métricas dependerem da ordem em que o conjunto de dados
chegou – as mesmas linhas, embaralhadas, moveram selective_accuracy@50 entre 0.000 e 1.000.
aurc integra um valor de risco por nível distinto, ponderado pelas respostas que aquele nível
abrange, então continua sendo uma área sob a curva risco–cobertura, e não uma média de pontos de
tamanhos desiguais.
Duas consequências para as quais vale planejar:
- Um número pode se mover em qualquer direção, por mais do que uma reordenação poderia. Onde um
grupo atravessa o corte, a leitura por limiar difere de toda leitura por índice de linha dos mesmos
dados: medido em 400.001 conjuntos de dados em forma de empate, até 0.500 para
selective_accuracy@50e 0.351 paraaurc. Na forma de doze respostas acima – seis corretas, todas com confiança 1.0 –aurcse move 0.327 (0.173 a 0.500). Um gate que estava passando pode falhar, e um que estava falhando pode passar; o veredito anterior dependia da ordem das linhas, inclusive para um limiteminoumaxabsoluto, que nada recusa porque lê uma única execução. - Regenere as linhas de base commitadas.
config.coverage_metric_definitionregistra qual definição produziu um relatório. Comparar uma métrica de cobertura entre duas definições é recusado em ambos os gates que subtraem uma linha de base –--baseline --tolerance(EvalReport.compare) e uma regra relativa (max_drop/max_increase) sob--gate-policy– e em qualquer dos lados estar desatualizado, não só a linha de base: um candidato produzido por umlayamais antigo carrega um artefato de ordem de linha que pode ler melhor que a verdade, então barrá-lo contra uma linha de base regenerada corretamente passaria uma regressão que o relatório pontuado corretamente reprova. Sem essa recusa, uma linha de base desatualizada esconde uma regressão real: uma fatia registrada em 0.033 sob a definição antiga lê 0.517 sob esta, então um candidato que caiu genuinamente 0.217 passaria por ummax_dropde 0.05.eceebriernão cortam e continuam comparáveis.
Sem empates nos dados há um nível por resposta, e ambas as métricas são exatamente o que sempre foram – idênticas bit a bit, não apenas próximas.
Adicione ScoreWithin(0.25) à lista de avaliadores para uma métrica de tolerância; o conjunto padrão
é choice_accuracy, noul_accuracy, score_mae, mean_confidence, mais ece. Pela CLI a mesma
coisa é uma flag: laya-evals run data.jsonl --score-within 0.25 reporta score_within_0.25 ao lado
dos padrões, e a flag se repete, então --score-within 0.25 --score-within 0.5 reporta os dois.
Uma métrica de tolerância precisa de uma resposta score com rótulo numérico, então em um conjunto
de dados sem uma ela não tem valor: run nomeia a métrica que não conseguiu calcular em vez de
publicar um zero silencioso, e um gate --min / --max que nomeie essa métrica falha como ausente.
As tolerâncias que uma execução pediu são registradas no bloco config do relatório, então uma linha
de base revisada diz quais colunas ela espera.
Batching e medição de tempo
--batch-size N pontua até N linhas consecutivas que compartilham um checkpoint e um esquema de
perguntas em uma única chamada. As duas métricas de tempo vêm das mesmas medições e respondem a
perguntas diferentes: toda linha de um lote retorna quando o lote retorna, então sua latency é a
chamada inteira, enquanto seu cost_per_decision é 1/N dela. O batching, portanto, aumenta
latency_* e reduz cost_per_decision_* em um conjunto inalterado de decisões, e
--max latency_p50_ms=... pergunta se as solicitações foram atendidas rápido, não se a execução foi
barata. Sem --batch-size os dois concordam.
compare ignora qualquer métrica *_ms a menos que uma tolerância a nomeie, então essas nunca
falham uma linha de base por ruído de tempo. O que o harness de fato fez — o tamanho de lote pedido,
o formato de runner em que ele se resolveu, quantas linhas compartilharam uma chamada e o maior
pedaço — é registrado em config.timing do relatório, porque a flag sozinha não diz se algo foi
agrupado em lote. Esses contadores registram as chamadas emitidas, não as chamadas que retornaram:
com laya-evals run --on-error skip, um pedaço cuja chamada lançou ainda conta em rows_grouped e
max_chunk, ao lado de suas entradas em config.errored. O padrão é --on-error fail, que relança
em vez de publicar um relatório cujas métricas cobrem apenas as chamadas que voltaram. As duas
métricas *_ms contam apenas as chamadas que retornaram, então uma chamada falha nunca contribui com
uma latência que ela não mediu.
Agrupar as linhas dentro de um lote
--sort-by-length agrupa linhas de tamanhos semelhantes na mesma passada direta, então cada passada
preenche até um máximo mais curto em vez de até a linha mais longa que há nela. É o formato das
chamadas, não as suas respostas: os resultados voltam na mesma ordem e pontuam de forma idêntica, que
é por que research/ consegue reportar 2.15x sobre 10.000 tickets sem nenhuma decisão mudar.
Precisa haver mais de uma passada para reordenar, então só tem efeito com um --batch-size N abaixo
do número de linhas que a execução agrupa. config.timing mantém as duas afirmações separadas:
sort_by_length é o que a linha de comando disse, sort_by_length_sent é o que chegou ao runner.
Uma execução sem --batch-size pede algo que não pode acontecer, e diz isso com sent: false; um
runner cujo predict_batch é anterior ao ajuste é pontuado sem ordenação em vez de lançar
TypeError no meio de uma execução longa.
O gate de abstenção em um limiar
--min-confidence T repassa o limiar de abstenção opt-in do core (#361) para toda chamada que a
execução faz, então o Router e o ONNXAgent marcam respostas cujo answer_confidence fica abaixo
de T com low_confidence: True antes de o harness vê-las. Diferente do agrupamento, isso muda as
respostas que pontuam: a mesma execução com T=0 e T=0.7 é um experimento diferente, e uma
varredura de precision@coverage é uma série destes, não uma única linha de base à deriva.
O intervalo aceito é o laya.confidence.check_min_confidence do core – [0.0, 1.0], finito, não
um bool – em vez de uma cópia aqui, então um valor que o próprio gate recusaria falha como erro de
uso (saída 2) antes de qualquer checkpoint carregar. 0.0 é um pedido legal: é o braço de controle
de uma varredura de precision@coverage, e uma verificação que o descartasse esconderia o próprio
piso da varredura.
Um runner cujo predict ou (para uma execução em lote) cujo predict_batch é anterior ao gate é
recusado com um EvalError nomeado, não pontuado sem o limiar. Descartar silenciosamente um
controle de pontuação é a classe de mentira que este harness existe para prevenir: o relatório
publicaria um número de precision@coverage para uma política que nunca rodou. config.timing
registra tanto o pedido quanto o fato: min_confidence é o limiar que foi solicitado,
min_confidence_sent diz se alguma chamada desta execução de fato o carregou.
Fatias
compare e run reportam os números gerais e, para --slice language|model|qid|tag, as mesmas
métricas por valor de fatia, então uma regressão em um idioma ou em uma pergunta fica visível sem ler
o agregado. A fatia model guarda o checkpoint que respondeu cada linha: a escolha do próprio
Router por solicitação, ou o model do runner para um runner que não roteia.
Gates de fatia opt-in
O gate de linha de base geral pode passar enquanto uma fatia menor de idioma ou pergunta regride.
Para tornar uma fatia revisada um requisito de CI, salve uma política JSON como gates.json:
{
"version": 1,
"rules": [
{"slice": {"language": "zh"}, "metric": "choice_accuracy",
"min_count": 50, "max_drop": 0.05},
{"slice": {"qid": "intent"}, "metric": "ece",
"min_count": 50, "max": 0.10}
]
}
laya-evals run data.jsonl --baseline baseline.json --tolerance choice_accuracy=0.02 \
--gate-policy gates.json --json report.json
laya-evals compare report.json --baseline baseline.json \
--tolerance choice_accuracy=0.02 --gate-policy gates.json
Cada regra seleciona exatamente um valor de language, model, qid ou tag e nomeia a métrica
exatamente como ela aparece no relatório de fatia. Ela tem um min_count positivo e exatamente um
limite: min ou max verifica o valor candidato; max_drop permite no máximo aquela diminuição em
relação à linha de base; max_increase permite no máximo aquele aumento. Os dois últimos exigem
--baseline. A contagem é o número de respostas pontuadas para aquela métrica na fatia selecionada,
em ambos os relatórios para uma regra relativa. Para ece, é o número de respostas com
confiança finita e valor correct booleano. Uma fatia ou métrica ausente, poucas respostas
pontuadas, ou casos pulados/com erro reprovam o gate opt-in. Regras relativas também exigem que ambos
os relatórios carreguem identidades de execução correspondentes, para que evidência ausente não possa
aparecer como um passe. Uma regressão medida reporta a fatia, a métrica, as contagens, os valores e o
limite. Sintaxe de política inválida sai com 2 antes de um checkpoint carregar; uma falha de qualidade
sai com 1. A política é registrada em config.gate_policy de um relatório de run --json.
compare --gate-policy aplica a política fornecida naquela linha de comando às medições salvas. Se
ela difere da política registrada no relatório, o compare diz isso; uma reverificação explícita sob
uma nova política não muda a política sob a qual a execução original foi feita.
A comparação geral regular ainda se aplica, incluindo sua tolerância e o comportamento de linha de
base legada. Sem --gate-policy, o relato e a comparação de fatias se comportam como antes.
Identidade da execução
run registra o que mediu no bloco config do relatório, então o artefato que um revisor lê é
revisável por si só:
| chave | significado |
|---|---|
schema |
o formato do relatório, laya-evals-report/1, para um consumidor poder recusar um que não consegue ler |
dataset |
o caminho como digitado – um nome, não um hash |
dataset_sha256 |
o sha256 dos bytes do conjunto de dados que foram analisados |
questions_sha256 |
uma impressão digital do esquema de perguntas: id, tipo, instructions e criteria de cada pergunta, sobre todo o conjunto de dados |
laya_version |
o laya que calculou os números |
coverage_metric_definition |
qual definição de aurc / selective_accuracy@* produziu este relatório (veja cortes de cobertura). Uma regra de gate relativa sobre qualquer um deles recusa uma linha de base registrada sob uma definição diferente, em vez de subtrair números que não significam a mesma coisa |
thresholds |
o gate que esta execução aplicou: min, max e baseline_tolerance |
gate_policy |
a política opcional de gate de fatia aplicada por run --gate-policy |
revisions |
o commit de onde cada checkpoint que respondeu foi carregado (veja abaixo) |
dataset é um caminho, e um caminho não é uma identidade: um conjunto de dados pode ser editado no
lugar, movido ou baixado de novo sob o mesmo nome, e um cache de CI pode entregar a duas execuções o
mesmo nome de arquivo e bytes diferentes. questions_sha256 cobre o que foi perguntado em vez de
quantas linhas havia, então adicionar estados a um conjunto de perguntas inalterado deixa a impressão
digital intacta — dataset_sha256 ainda se move, e adicionar uma linha é uma mudança nos dados, não
na pergunta.
Ele cobre instructions também, porque o texto da instrução é o prompt. build_sequence renderiza
"<type> question: <instructions>" na cabeça tokenizada, o Agent recusa uma pergunta sem uma
(“adicione o texto que o modelo deve responder”), e a própria identidade de pergunta do Laya já a
conta: Router._question_schema e o agrupamento em lote deste harness usam como chave o dict de
perguntas inteiro, e tests/test_router_batch.py fixa que reformular apenas instructions move uma
linha para o seu próprio grupo de lote. Uma instrução reformulada ainda se compara igual a uma linha
de base? Não — e esse é o ponto. “Julgue se um reembolso é justificado” e “Seja conservador e só
aprove pedidos de reembolso explícitos” fazem perguntas diferentes, e o gate de métrica só consegue
notar quando a diferença por acaso move um número além da tolerância que você nomeou. Nomear uma
opção de choice é o mesmo argumento: criteria é o espaço de decisão, e os checks metamórficos em
research/eval/metamorphic.py existem porque renomear um rótulo vira as respostas.
Nada do texto da instrução é normalizado exceto o único passo que o próprio motor aplica: uma
instructions não textual é transformada em hash como json.dumps(ins, ensure_ascii=False),
correspondendo a Agent._to_internal. Então espaços em branco e redação contam, e uma reformulação
que um humano considera uma edição de cópia é tratada como um novo experimento. Esse é o padrão
honesto — a alternativa é uma heurística de similaridade entre uma execução e sua linha de base, e
nenhum sistema de avaliação de uso comum tem uma.
Nada que carregue tempo é registrado, então um relatório continua reproduzível byte a byte para um runner fixo.
REPORT_SCHEMA, questions_fingerprint(dataset) e file_fingerprint(path) são públicos, então um
chamador que usa laya.evals.evaluate diretamente obtém a mesma identidade que uma execução da CLI.
Linha de base e gate de CI
- Mantenha o conjunto de dados, um relatório de linha de base (saída
--jsonque você revisou) e as tolerâncias juntos, com commit, para que uma mudança seja um diff revisável.--tolerance METRIC=VALUEé o desvio absoluto máximo permitido para aquela métrica. laya-evals run ... --baseline baseline.json --tolerance ...sai com código diferente de zero em caso de desvio, então cai direto em CI sem mudança.laya.evals.EvalReport.compareeassert_regressionexpõem a mesma lógica para testes.
O gate de métrica responde “os números se moveram”. Ele não consegue responder “eram os mesmos
números”, porque compare lê overall e apenas overall — então uma linha de base registrada
contra um conjunto de dados passaria por um candidato pontuado em outro, com aritmética idêntica.
EvalReport.comparable_to fecha isso: ele compara schema, dataset_sha256 e questions_sha256, e
run --baseline e compare ainda imprimem todo delta, então falham com saída diferente de zero
nomeando a chave e os dois valores:
FAIL: baseline is not comparable: dataset_sha256 (dataset bytes): baseline is <sha>, this run is <sha>
Uma chave ausente em qualquer dos lados é desconhecida, não um conflito, então todo relatório
escrito antes de a identidade existir continua comparando exatamente como antes. Isso inclui a linha
de base do gate agendado abaixo, que vem de research/eval/ e não tem nenhum config.schema.
Duas superfícies de CI usam isto:
- um job sem pesos em
.github/workflows/ci.ymlrodatests/test_evals.pyetests/test_evals_api.py, então a matemática das métricas, o parsing do conjunto de dados e a CLI ficam cobertos em todo PR sem baixar um checkpoint; .github/workflows/evals.ymlroda semanalmente, antes de uma versão e sob demanda: ele avalia o checkpoint em inglês na suíte MASSIVE em inglês e compara comresearch/results/eval_english_51_languages.jsoncom as tolerâncias emresearch/evals/thresholds.json. Ele sobe o relatório como artefato e não bloqueia um PR.
O harness é determinístico para uma revisão de checkpoint fixa, então um relatório é reproduzível.
run registra o conjunto de dados, o modelo e o dispositivo, mais os fatos de
tempo da execução, no bloco config do relatório, e revisions: o
commit de onde cada checkpoint que respondeu foi de fato carregado. --revision <SHA> fixa esse
commit para todo checkpoint que a execução carrega, e --revision english=<SHA> fixa um checkpoint
(repetível) — que é a forma que uma execução com roteamento automático quer, já que os três
checkpoints são três repositórios e um commit não pode existir em todos eles. Deixado sem fixar, a
execução pega o branch padrão do checkpoint e o relatório ainda diz qual commit respondeu, então um
desvio de linha de base pode ser atribuído aos pesos ou ao código. laya/revisions.py publica SHAs
de commit revisados em PINNED_REVISIONS para chamadores que querem optar. Com --onnx, apenas um
--revision <SHA> puro se aplica, ao download da config e do tokenizer.
Adicionar o conjunto rotulado real
Coloque um JSONL em research/evals/ e uma linha de base revisada ao lado dele, depois aponte um
fluxo de trabalho (ou research/evals/check_regression.py) para os dois. O formato é o mesmo do
fixture; nada no harness sabe de MASSIVE.