diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17523ff..0db9a93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,6 @@ name: CAFUNE CI on: push: - branches: [main] pull_request: branches: [main] @@ -42,3 +41,5 @@ jobs: run: julia --project=julia -e 'using Pkg; Pkg.instantiate()' - name: Run canonical smoke test run: julia --project=julia julia/smoke_test.jl + - name: Run CAFUNE-mini training sanity check + run: julia --project=julia julia/main_training.jl --config config/experiments/cafune-mini.toml --sanity diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a99c298 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# CAFUNE — guia de colaboração + +## Fluxo Git e releases + +- `main` é a linha estável: mantenha-a verde no CI e não faça commits diretos. +- Todo trabalho começa em uma branch publicada e integrada por pull request. +- Use `feature/` para capacidades ou experimentos, por exemplo + `feature/cafune-mini`, `feature/mha-baseline` e `feature/tokenizer-ablation`. +- Use `fix/` para correções. Para uma correção urgente de uma release + publicada, use `hotfix/`. +- Releases são tags semânticas (`v0.1.0`, `v0.1.1`). Crie `release/vX.Y` apenas + quando for necessário manter correções nessa linha sem incluir trabalho novo. +- Não misture hipóteses experimentais diferentes na mesma branch ou PR. + +## Contratos técnicos + +- A implementação canônica é `julia/src/`; não reviva `julia/train_unified.jl` + nem lançadores legados como uma segunda fonte do modelo. +- `config/research.toml` define a configuração de pesquisa canônica. +- `config/experiments/cafune-mini.toml` é a configuração de ~7,07M parâmetros + para ablações. Preserve tokenizer, dados e splits ao comparar mini vs baseline. +- `python/dataset_splits.json` é o contrato de splits determinísticos. Se o + dataset tokenizado mudar, regenere os splits antes de treinar ou avaliar. +- O melhor checkpoint é escolhido por `aggregate.loss_mean` da avaliação de + validação; nunca por loss de treino. +- Antes de iniciar um treino comparável, registre hipótese e controles em + `docs/EXPERIMENTS.md`. Só registre conclusões após existir avaliação + reproduzível. +- Checkpoints, avaliações geradas, logs, caches e mmap são artefatos de runtime, + não código-fonte. + +## Verificação + +Antes de abrir PR, rode o que estiver disponível: + +```powershell +python python/verify_research.py +python -m pytest python/tests -q +julia --project=julia julia/smoke_test.jl +``` + +Se Julia não estiver disponível localmente, não declare a alteração validada +somente por inspeção: publique a branch e aguarde o GitHub Actions concluir. +Não inicie treino, RLAIF ou chamadas externas como parte da verificação. diff --git a/README.md b/README.md index 1b7da25..74c267d 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,14 @@ julia --project=julia julia/evaluate.jl --checkpoint julia/checkpoints/cafune_be O melhor checkpoint do treino é selecionado por essa `validation loss` agregada, nunca pela loss de treino. Os relatórios ficam em `julia/evaluations/`. +Para experimentar hipóteses sem treinar o modelo de 45M, use o CAFUNÉ-mini +(~7,07M parâmetros). Ele preserva tokenizer, dados e splits da baseline e isola +seus artefatos em `julia/checkpoints/cafune-mini/`: + +```powershell +julia --project=julia julia/main_training.jl --config config/experiments/cafune-mini.toml --sanity +``` + Dashboard local: ```powershell diff --git a/config/experiments/cafune-mini.toml b/config/experiments/cafune-mini.toml new file mode 100644 index 0000000..49e9568 --- /dev/null +++ b/config/experiments/cafune-mini.toml @@ -0,0 +1,36 @@ +# Configuração para experimentos e ablações baratos. +# Mantém tokenizer, dados e sequência da baseline de 45M; reduz apenas capacidade. + +[model] +name = "cafune-mini-7m" +vocab_size = 1999 +seq_len = 128 +d_model = 256 +n_heads = 8 +n_layers = 8 +d_ff = 1024 +dropout = 0.1 +linear_mode = "float32" + +[tokenizer] +type = "sentencepiece_bpe" +model = "python/cafune_spm.model" +vocab = "python/vocab_spm.json" +config = "python/spm_config.json" +dataset = "python/dataset_tokens.json" +pad_id = 0 +unk_id = 1 +bos_id = 2 +eos_id = 3 +mask_id = 4 + +[data] +splits = "python/dataset_splits.json" +train_ratio = 0.90 +validation_ratio = 0.05 +test_ratio = 0.05 +split_seed = "cafune-v1" + +[experiment] +purpose = "cheap_architecture_and_tokenizer_ablations" +parameters = 7071744 diff --git a/docs/EXPERIMENTS.md b/docs/EXPERIMENTS.md new file mode 100644 index 0000000..48adf82 --- /dev/null +++ b/docs/EXPERIMENTS.md @@ -0,0 +1,34 @@ +# Registro de experimentos + +Este arquivo preserva a trilha científica do CAFUNE. Registre uma entrada antes +de iniciar qualquer treino que produza resultado comparável. + +Cada entrada deve declarar hipótese, configuração, dados/splits, sementes, +métrica de decisão, estado e links para artefatos. Não promova uma hipótese a +conclusão sem um relatório de avaliação reproduzível. + +## EXP-001 — CAFUNE-mini como bancada de ablações + +- **Hipótese:** uma configuração de aproximadamente 7M parâmetros permite + comparar escolhas arquiteturais antes de escalar para o modelo canônico de + 45M. +- **Branch:** `feature/cafune-mini`. +- **Configuração:** `config/experiments/cafune-mini.toml` — 8 camadas, + `d_model=256`, 8 heads, `d_ff=1024`, 7.071.744 parâmetros. +- **Controles:** SentencePiece BPE 1.999, `seq_len=128`, + `python/dataset_tokens.json` e `python/dataset_splits.json` idênticos à + baseline canônica. +- **Métrica de decisão:** `aggregate.loss_mean` no split de validação, com + máscaras em 0,10 / 0,25 / 0,50 / 0,75 / 0,90 e três seeds. +- **Estado:** pronto para treino; sanity training e CI passaram. Ainda não há + métrica de qualidade ou conclusão empírica. +- **Próxima comparação:** MHA-only versus MHA+SSA, com esta configuração e os + mesmos controles. + +## Convenções de artefatos + +- Checkpoints: `julia/checkpoints//`. +- Avaliações: `julia/evaluations/`; preserve no JSON a configuração, checkpoint, + split, hash do dataset e seeds. +- Resultados agregados e interpretação entram neste arquivo após confirmação de + reprodutibilidade. Logs brutos não pertencem ao Git. diff --git a/julia/main_training.jl b/julia/main_training.jl index 25045e7..4a7a310 100644 --- a/julia/main_training.jl +++ b/julia/main_training.jl @@ -92,16 +92,27 @@ const SCRIPT_DIR = @__DIR__ const SANITY_MODE = "--sanity" in ARGS const BITNET_MODE = "--bitnet" in ARGS const RLAIF_MODE = "--rlaif" in ARGS -const RESEARCH_CONFIG = normpath(joinpath(SCRIPT_DIR, "..", "config", "research.toml")) +const DEFAULT_RESEARCH_CONFIG = normpath(joinpath(SCRIPT_DIR, "..", "config", "research.toml")) +const RESEARCH_CONFIG = let index = findfirst(==("--config"), ARGS) + if index === nothing + DEFAULT_RESEARCH_CONFIG + else + index < length(ARGS) || error("--config requer um caminho para um arquivo TOML.") + config_path = ARGS[index + 1] + normpath(isabspath(config_path) ? config_path : joinpath(pwd(), config_path)) + end +end +const EXPERIMENT_NAME = RESEARCH_CONFIG == DEFAULT_RESEARCH_CONFIG ? "baseline" : splitext(basename(RESEARCH_CONFIG))[1] const MEM_FILE = normpath(joinpath(SCRIPT_DIR, "..", "cafune_brain.mem")) const CORPUS_FILE = normpath(joinpath(SCRIPT_DIR, "..", "python", "social_data.json")) const VOCAB_FILE = normpath(joinpath(SCRIPT_DIR, "..", "vocab.json")) const SPM_CONFIG = normpath(joinpath(SCRIPT_DIR, "..", "python", "spm_config.json")) const SPM_TOKENS = normpath(joinpath(SCRIPT_DIR, "..", "python", "dataset_tokens.json")) const DATA_SPLITS = normpath(joinpath(SCRIPT_DIR, "..", "python", "dataset_splits.json")) -const VARIANT_DIR = BITNET_MODE ? "bitnet" : "baseline" -const CKPT_DIR = BITNET_MODE ? joinpath(SCRIPT_DIR, "checkpoints", SANITY_MODE ? "sanity" : "", "bitnet") : - (SANITY_MODE ? joinpath(SCRIPT_DIR, "checkpoints", "sanity") : joinpath(SCRIPT_DIR, "checkpoints")) +const VARIANT_DIR = BITNET_MODE ? "bitnet" : EXPERIMENT_NAME +const CKPT_DIR = BITNET_MODE ? joinpath(SCRIPT_DIR, "checkpoints", SANITY_MODE ? "sanity" : "", EXPERIMENT_NAME, "bitnet") : + SANITY_MODE ? (EXPERIMENT_NAME == "baseline" ? joinpath(SCRIPT_DIR, "checkpoints", "sanity") : joinpath(SCRIPT_DIR, "checkpoints", "sanity", EXPERIMENT_NAME)) : + EXPERIMENT_NAME == "baseline" ? joinpath(SCRIPT_DIR, "checkpoints") : joinpath(SCRIPT_DIR, "checkpoints", EXPERIMENT_NAME) const BEST_CKPT = joinpath(CKPT_DIR, "cafune_best.bson") const TRAIN_LOG = SANITY_MODE ? joinpath(CKPT_DIR, "training_log.jsonl") : joinpath(SCRIPT_DIR, "training_log.jsonl") diff --git a/python/tests/test_research.py b/python/tests/test_research.py index ea86ce4..049a55d 100644 --- a/python/tests/test_research.py +++ b/python/tests/test_research.py @@ -1,10 +1,11 @@ from pathlib import Path import sys +import tomllib sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from verify_research import DEFAULT_CONFIG, audit, parameter_count +from verify_research import DEFAULT_CONFIG, ROOT, audit, parameter_count def test_canonical_model_exceeds_45_1m_parameters(): @@ -19,3 +20,14 @@ def test_research_contract_has_no_failures(): "architecture", "tokenizer", } + + +def test_cafune_mini_configuration_is_a_7m_ablation_model(): + config_path = ROOT / "config" / "experiments" / "cafune-mini.toml" + model = tomllib.loads(config_path.read_text(encoding="utf-8"))["model"] + assert parameter_count( + vocab_size=model["vocab_size"], + d_model=model["d_model"], + n_layers=model["n_layers"], + d_ff=model["d_ff"], + ) == 7_071_744