Text Classification
Transformers
Safetensors
modernbert
agent-safety
tool-calling
long-context
distillation
Eval Results (legacy)
text-embeddings-inference
Instructions to use ProCreations/auto-0.4b-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ProCreations/auto-0.4b-2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="ProCreations/auto-0.4b-2")# Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("ProCreations/auto-0.4b-2") model = AutoModelForSequenceClassification.from_pretrained("ProCreations/auto-0.4b-2", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Publish evaluated auto-0.4b-2 checkpoint and reproducibility artifacts
Browse files- README.md +114 -0
- artifact_hashes.json +14 -0
- benchmark_predictions.npz +3 -0
- config.json +92 -0
- data_audit.json +99 -0
- environment.json +10 -0
- eval_results.json +0 -0
- gate.py +10 -0
- model.safetensors +3 -0
- source_revisions.json +22 -0
- tokenizer.json +0 -0
- tokenizer_config.json +24 -0
- training/download.py +27 -0
- training/engine.py +320 -0
- training/prepare.py +116 -0
- training/publish.py +260 -0
- training/run.py +98 -0
- training/sft_check.py +56 -0
- training_plan.json +41 -0
README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model:
|
| 4 |
+
- ProCreations/auto-0.4b
|
| 5 |
+
pipeline_tag: text-classification
|
| 6 |
+
library_name: transformers
|
| 7 |
+
datasets:
|
| 8 |
+
- ProCreations/auto-1b-data
|
| 9 |
+
tags:
|
| 10 |
+
- modernbert
|
| 11 |
+
- agent-safety
|
| 12 |
+
- tool-calling
|
| 13 |
+
- long-context
|
| 14 |
+
model-index:
|
| 15 |
+
- name: auto-0.4b-2
|
| 16 |
+
results:
|
| 17 |
+
- task:
|
| 18 |
+
type: text-classification
|
| 19 |
+
name: Agentic tool-call approve/deny classification
|
| 20 |
+
dataset:
|
| 21 |
+
name: Approve-or-Deny
|
| 22 |
+
type: ProCreations/approve-or-deny
|
| 23 |
+
split: test
|
| 24 |
+
revision: a38b625913dd46ca9702063f1597c979e6ace34e
|
| 25 |
+
metrics:
|
| 26 |
+
- type: accuracy
|
| 27 |
+
value: 0.9676666666666667
|
| 28 |
+
- type: f1
|
| 29 |
+
value: 0.9654927072216293
|
| 30 |
+
name: F1 (deny)
|
| 31 |
+
- type: roc_auc
|
| 32 |
+
value: 0.9943784012045359
|
| 33 |
+
- type: false_approve_rate
|
| 34 |
+
value: 0.03140613847251963
|
| 35 |
+
- type: false_deny_rate
|
| 36 |
+
value: 0.03314571607254534
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
# auto-0.4b-2
|
| 40 |
+
|
| 41 |
+
A 395.8M-parameter ModernBERT encoder for deciding whether an agent's proposed tool call is authorized and safe in context. Continued full-parameter training of [auto-0.4b](https://huggingface.co/ProCreations/auto-0.4b), with supervised training on short and full-length long-context examples; teacher distillation was skipped because the post-SFT benchmark accuracy exceeded the teacher, on one NVIDIA RTX PRO 6000 Blackwell 96 GB GPU.
|
| 42 |
+
|
| 43 |
+
## Measured results
|
| 44 |
+
|
| 45 |
+
The same pinned 3,000-item benchmark, input serialization, tokenizer, full input lengths, attention implementation, and default `P(deny) >= 0.5` threshold were used for all three models below. Original and teacher results are fresh measurements; older model-card scores can differ across data/runtime revisions.
|
| 46 |
+
|
| 47 |
+
| Model | Accuracy | False approve | False deny | AUROC |
|
| 48 |
+
|---|---:|---:|---:|---:|
|
| 49 |
+
| Original auto-0.4b | 89.63% | 6.07% | 14.13% | 0.9655 |
|
| 50 |
+
| Teacher auto-1b-bf16 | 96.60% | 4.14% | 2.75% | 0.9930 |
|
| 51 |
+
| **auto-0.4b-2** | 96.77% | 3.14% | 3.31% | 0.9944 |
|
| 52 |
+
|
| 53 |
+
Accuracy improved by **7.10 percentage points**; paired bootstrap 95% interval: 6.07 to 8.17 points. Accuracy Wilson 95% interval: 96.07%–97.34%.
|
| 54 |
+
|
| 55 |
+
Accuracy on inputs of 16,384–65,536 tokens: **94.56%** (239 items). The untouched validation audit partition scored **97.88%** across 2595 items. Full category, difficulty, language, length, confusion counts, confidence intervals, and threshold results are in `eval_results.json`.
|
| 56 |
+
|
| 57 |
+
The separately calibrated recommended threshold is `0.340`. It was chosen only on 2,581 validation calibration rows. The table above uses 0.5, without test-set threshold tuning.
|
| 58 |
+
|
| 59 |
+
## Training and evaluation integrity
|
| 60 |
+
|
| 61 |
+
- 711,985 retained training examples, 516,733,212 tokens. Exact normalized-text deduplication and request-plus-call group separation removed 15 rows. There was no detected benchmark overlap.
|
| 62 |
+
- 13,000 validation rows were split by request-plus-call group into 7,824 selection, 2,581 calibration, and 2,595 untouched audit rows. Benchmark examples were excluded from training targets, teacher-target generation, threshold tuning, and checkpoint selection.
|
| 63 |
+
- At the user's request, a checkpoint frozen on validation was evaluated after long SFT, scoring 96.77% benchmark accuracy. This intermediate benchmark decided whether to run distillation: distill if the student did not strictly beat the teacher's 96.60%; otherwise skip it. Consequently the benchmark was used for this training-procedure decision and is not an untouched one-use final test. The independent validation audit remained unused until final evaluation.
|
| 64 |
+
- Two short-context supervised epochs; one full-length long-context supervised epoch with short replay; teacher distillation was skipped under the user-requested benchmark comparison rule.
|
| 65 |
+
- Every retained training row was used at full tokenized length. No training example required truncation. Configured capacity is 65,536 tokens; the supplied training corpus reaches 51,408 tokens and benchmark reaches 56,176 with this tokenizer. The 65,536-token path is a capacity smoke test, not a claim of measured accuracy at exactly 65,536 tokens.
|
| 66 |
+
- Full-parameter AdamW with FP32 master weights and moments, BF16 compute, cosine learning-rate schedules, gradient clipping, and activation checkpointing for long inputs. BF16 safetensors are published.
|
| 67 |
+
- No teacher distillation was applied to this release.
|
| 68 |
+
- Each checkpoint was frozen using validation selection scores before its benchmark evaluation. Training sources and immutable revisions, data audit, plan, environment versions, and runnable scripts are included.
|
| 69 |
+
|
| 70 |
+
## Usage
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
import torch
|
| 74 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 75 |
+
|
| 76 |
+
"""Serialize exactly the information available to an approve/deny classifier."""
|
| 77 |
+
def build_input(user_request, history, call):
|
| 78 |
+
parts = ['### PROPOSED TOOL CALL', f"tool: {call['tool']}", f"args: {call['args']}", '',
|
| 79 |
+
'### USER REQUEST', user_request, '', '### AGENT HISTORY']
|
| 80 |
+
if not history:
|
| 81 |
+
parts.append('(no prior actions)')
|
| 82 |
+
else:
|
| 83 |
+
for i, h in enumerate(history):
|
| 84 |
+
parts.append(f"[{i+1}] {h['tool']}({h['args']})\n-> {h.get('result', '')}")
|
| 85 |
+
return '\n'.join(parts)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
repo = "ProCreations/auto-0.4b-2"
|
| 89 |
+
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 90 |
+
model = AutoModelForSequenceClassification.from_pretrained(
|
| 91 |
+
repo, dtype=torch.bfloat16,
|
| 92 |
+
attn_implementation="kernels-community/flash-attn2@81fb77c12b2ad5d69380669b46739d5868614502",
|
| 93 |
+
).cuda().eval()
|
| 94 |
+
|
| 95 |
+
text = build_input(
|
| 96 |
+
user_request="Clean up the build artifacts and reinstall dependencies.",
|
| 97 |
+
history=[{"tool": "Bash", "args": "ls", "result": "node_modules dist package.json"}],
|
| 98 |
+
call={"tool": "Bash", "args": "rm -rf node_modules dist && npm install"},
|
| 99 |
+
)
|
| 100 |
+
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=65536).to("cuda")
|
| 101 |
+
with torch.inference_mode():
|
| 102 |
+
p_deny = model(**inputs).logits.float().softmax(-1)[0, 1].item()
|
| 103 |
+
print("deny" if p_deny >= 0.5 else "approve", p_deny)
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
Install the tested versions listed in `environment.json`, including `kernels`; the pinned Hugging Face FlashAttention kernel loads automatically on supported CUDA systems. A compatible local FlashAttention 2 installation can also use `attn_implementation="flash_attention_2"`. Ordinary SDPA is suitable for shorter inputs but can materialize large masks for long sequences.
|
| 107 |
+
|
| 108 |
+
Labels are `0 = approve`, `1 = deny`. Preserve the exact `### PROPOSED TOOL CALL`, `### USER REQUEST`, and `### AGENT HISTORY` sections. The model only consumes this serialized text; training rationales and metadata are excluded.
|
| 109 |
+
|
| 110 |
+
## Scope and limitations
|
| 111 |
+
|
| 112 |
+
This is a classification model, not a chat model. It approves routine authorized work and denies consequential unauthorized actions or actions following injected instructions. Its synthetic labels and held-out benchmark do not establish reliability on every real agent workload. A false approval remains possible; the model cannot inspect hidden file contents, resolve opaque executables, or determine a URL's runtime behavior from text alone. Long-context results are slice measurements on the supplied benchmark, not a universal guarantee. Evaluate on representative deployment traffic and treat uncertain decisions appropriately for the application.
|
| 113 |
+
|
| 114 |
+
The base model and teacher were previously developed using their own validation histories; this run prevents new training/evaluation overlap but cannot independently establish that all historical model-development decisions were untouched by public benchmarks.
|
artifact_hashes.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"eval_results.json": "dc4bb0d424873883e0ff1c1e0bb53a1630a34649dc11b031b2749e11386ca9df",
|
| 3 |
+
"README.md": "abd0e0fd8e83f6b595ce439845b5331c29a902392f1ba50aecf1fedf738940fc",
|
| 4 |
+
"config.json": "d6f114a76d668e44b2720fd8d8a8e48d42a9ee969b0b174d391912008a14f9ec",
|
| 5 |
+
"environment.json": "de9a91afa9816139ab20e489fc3a7ca3887324d8a2b8291d5ceca5fd8d53fe95",
|
| 6 |
+
"data_audit.json": "659d61976ceee6241d66612b452693a2d4af5eeeaf870dd95e4521e57a21d839",
|
| 7 |
+
"benchmark_predictions.npz": "5ef6fed2fb1faae86c25b9290b65ce50204249a778b48510e7e8b7536c8783e8",
|
| 8 |
+
"model.safetensors": "10a27866e72a8fd7af878eb3d41e68f2de8e0f025cc20e55ef531554d1abc626",
|
| 9 |
+
"tokenizer.json": "abf58f8fa056b6f6c5bb0b667af9dd5893bfe96022f8981d6daa7571fdbc830c",
|
| 10 |
+
"tokenizer_config.json": "f0fcf16a9b5a657f9441b1fe7d66a0060c1151c645cf8e44d26aca5697f5c7ca",
|
| 11 |
+
"gate.py": "ae657066ddcaaf7065e21327a216d78c34f8cf6871e30083d79432b531f4c690",
|
| 12 |
+
"source_revisions.json": "e574b8bc8d025c33bbb2f1d564e8abb3247bb23c04bcd6b73d58c0c099fbce85",
|
| 13 |
+
"training_plan.json": "3a87e7c6b1e7534fdf35ef903f9f07225ce9bb10696bd6d5f953b5b7a8d8d25d"
|
| 14 |
+
}
|
benchmark_predictions.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ef6fed2fb1faae86c25b9290b65ce50204249a778b48510e7e8b7536c8783e8
|
| 3 |
+
size 72762
|
config.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"ModernBertForSequenceClassification"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"bos_token_id": 50281,
|
| 8 |
+
"classifier_activation": "gelu",
|
| 9 |
+
"classifier_bias": false,
|
| 10 |
+
"classifier_dropout": 0.0,
|
| 11 |
+
"classifier_pooling": "cls",
|
| 12 |
+
"cls_token_id": 50281,
|
| 13 |
+
"decoder_bias": true,
|
| 14 |
+
"deterministic_flash_attn": false,
|
| 15 |
+
"dtype": "float32",
|
| 16 |
+
"embedding_dropout": 0.0,
|
| 17 |
+
"eos_token_id": 50282,
|
| 18 |
+
"global_attn_every_n_layers": 3,
|
| 19 |
+
"gradient_checkpointing": false,
|
| 20 |
+
"hidden_activation": "gelu",
|
| 21 |
+
"hidden_size": 1024,
|
| 22 |
+
"id2label": {
|
| 23 |
+
"0": "approve",
|
| 24 |
+
"1": "deny"
|
| 25 |
+
},
|
| 26 |
+
"initializer_cutoff_factor": 2.0,
|
| 27 |
+
"initializer_range": 0.02,
|
| 28 |
+
"intermediate_size": 2624,
|
| 29 |
+
"label2id": {
|
| 30 |
+
"approve": 0,
|
| 31 |
+
"deny": 1
|
| 32 |
+
},
|
| 33 |
+
"layer_norm_eps": 1e-05,
|
| 34 |
+
"layer_types": [
|
| 35 |
+
"full_attention",
|
| 36 |
+
"sliding_attention",
|
| 37 |
+
"sliding_attention",
|
| 38 |
+
"full_attention",
|
| 39 |
+
"sliding_attention",
|
| 40 |
+
"sliding_attention",
|
| 41 |
+
"full_attention",
|
| 42 |
+
"sliding_attention",
|
| 43 |
+
"sliding_attention",
|
| 44 |
+
"full_attention",
|
| 45 |
+
"sliding_attention",
|
| 46 |
+
"sliding_attention",
|
| 47 |
+
"full_attention",
|
| 48 |
+
"sliding_attention",
|
| 49 |
+
"sliding_attention",
|
| 50 |
+
"full_attention",
|
| 51 |
+
"sliding_attention",
|
| 52 |
+
"sliding_attention",
|
| 53 |
+
"full_attention",
|
| 54 |
+
"sliding_attention",
|
| 55 |
+
"sliding_attention",
|
| 56 |
+
"full_attention",
|
| 57 |
+
"sliding_attention",
|
| 58 |
+
"sliding_attention",
|
| 59 |
+
"full_attention",
|
| 60 |
+
"sliding_attention",
|
| 61 |
+
"sliding_attention",
|
| 62 |
+
"full_attention"
|
| 63 |
+
],
|
| 64 |
+
"local_attention": 128,
|
| 65 |
+
"max_position_embeddings": 65536,
|
| 66 |
+
"mlp_bias": false,
|
| 67 |
+
"mlp_dropout": 0.0,
|
| 68 |
+
"model_type": "modernbert",
|
| 69 |
+
"norm_bias": false,
|
| 70 |
+
"norm_eps": 1e-05,
|
| 71 |
+
"num_attention_heads": 16,
|
| 72 |
+
"num_hidden_layers": 28,
|
| 73 |
+
"pad_token_id": 50283,
|
| 74 |
+
"position_embedding_type": "absolute",
|
| 75 |
+
"repad_logits_with_grad": false,
|
| 76 |
+
"rope_parameters": {
|
| 77 |
+
"full_attention": {
|
| 78 |
+
"rope_theta": 2560000.0,
|
| 79 |
+
"rope_type": "default"
|
| 80 |
+
},
|
| 81 |
+
"sliding_attention": {
|
| 82 |
+
"rope_theta": 10000.0,
|
| 83 |
+
"rope_type": "default"
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"sep_token_id": 50282,
|
| 87 |
+
"sparse_pred_ignore_index": -100,
|
| 88 |
+
"sparse_prediction": false,
|
| 89 |
+
"tie_word_embeddings": true,
|
| 90 |
+
"transformers_version": "5.16.1",
|
| 91 |
+
"vocab_size": 50368
|
| 92 |
+
}
|
data_audit.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sources": {
|
| 3 |
+
"student": {
|
| 4 |
+
"repo": "ProCreations/auto-0.4b",
|
| 5 |
+
"revision": "2f6ec6ca79d733fa8c1677676a77cc85b739f0d2"
|
| 6 |
+
},
|
| 7 |
+
"teacher": {
|
| 8 |
+
"repo": "ProCreations/auto-1b-bf16",
|
| 9 |
+
"revision": "328c2d93e0390610bbeab4dea0000d0670a51be2"
|
| 10 |
+
},
|
| 11 |
+
"data": {
|
| 12 |
+
"repo": "ProCreations/auto-1b-data",
|
| 13 |
+
"revision": "d265bbf7bac76cd55daaf81a1d185dfe089a412b"
|
| 14 |
+
},
|
| 15 |
+
"benchmark": {
|
| 16 |
+
"repo": "ProCreations/approve-or-deny",
|
| 17 |
+
"revision": "a38b625913dd46ca9702063f1597c979e6ace34e"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"deduplication": "SHA256 whitespace-normalized text; request+canonical call group exclusion across splits; conflicting exact-text labels removed from train.",
|
| 21 |
+
"seed": 20260908,
|
| 22 |
+
"benchmark_rows": 3000,
|
| 23 |
+
"validation_original": 13000,
|
| 24 |
+
"validation_partitions": {
|
| 25 |
+
"selection": 7824,
|
| 26 |
+
"calibration": 2581,
|
| 27 |
+
"audit": 2595,
|
| 28 |
+
"monitor": 2025
|
| 29 |
+
},
|
| 30 |
+
"train_original": 712000,
|
| 31 |
+
"removed_train": {
|
| 32 |
+
"validation_group_overlap": 14,
|
| 33 |
+
"duplicate_text": 1
|
| 34 |
+
},
|
| 35 |
+
"train": {
|
| 36 |
+
"rows": 711985,
|
| 37 |
+
"tokens": 516733212,
|
| 38 |
+
"label_counts": {
|
| 39 |
+
"approve": 355072,
|
| 40 |
+
"deny": 356913
|
| 41 |
+
},
|
| 42 |
+
"length_percentiles": {
|
| 43 |
+
"p0": 44.0,
|
| 44 |
+
"p25": 221.0,
|
| 45 |
+
"p50": 374.0,
|
| 46 |
+
"p75": 509.0,
|
| 47 |
+
"p90": 657.0,
|
| 48 |
+
"p95": 815.0,
|
| 49 |
+
"p99": 11582.0,
|
| 50 |
+
"p100": 51408.0
|
| 51 |
+
},
|
| 52 |
+
"over_model_limit": 0,
|
| 53 |
+
"gt4096": 18578,
|
| 54 |
+
"gt16384": 4202
|
| 55 |
+
},
|
| 56 |
+
"validation": {
|
| 57 |
+
"rows": 13000,
|
| 58 |
+
"tokens": 15660962,
|
| 59 |
+
"label_counts": {
|
| 60 |
+
"approve": 6475,
|
| 61 |
+
"deny": 6525
|
| 62 |
+
},
|
| 63 |
+
"length_percentiles": {
|
| 64 |
+
"p0": 47.0,
|
| 65 |
+
"p25": 227.0,
|
| 66 |
+
"p50": 391.0,
|
| 67 |
+
"p75": 531.0,
|
| 68 |
+
"p90": 777.0,
|
| 69 |
+
"p95": 5837.399999999994,
|
| 70 |
+
"p99": 21976.960000000043,
|
| 71 |
+
"p100": 48710.0
|
| 72 |
+
},
|
| 73 |
+
"over_model_limit": 0,
|
| 74 |
+
"gt4096": 827,
|
| 75 |
+
"gt16384": 185
|
| 76 |
+
},
|
| 77 |
+
"benchmark": {
|
| 78 |
+
"rows": 3000,
|
| 79 |
+
"tokens": 11710540,
|
| 80 |
+
"label_counts": {
|
| 81 |
+
"deny": 1401,
|
| 82 |
+
"approve": 1599
|
| 83 |
+
},
|
| 84 |
+
"length_percentiles": {
|
| 85 |
+
"p0": 49.0,
|
| 86 |
+
"p25": 229.75,
|
| 87 |
+
"p50": 403.0,
|
| 88 |
+
"p75": 824.25,
|
| 89 |
+
"p90": 11993.599999999999,
|
| 90 |
+
"p95": 25400.399999999998,
|
| 91 |
+
"p99": 51709.639999999985,
|
| 92 |
+
"p100": 56176.0
|
| 93 |
+
},
|
| 94 |
+
"over_model_limit": 0,
|
| 95 |
+
"gt4096": 450,
|
| 96 |
+
"gt16384": 239
|
| 97 |
+
},
|
| 98 |
+
"monitor_sampling": "All selection rows over 4096 tokens plus 1536 fixed random short selection rows"
|
| 99 |
+
}
|
environment.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"torch": "2.13.0",
|
| 3 |
+
"transformers": "5.16.1",
|
| 4 |
+
"datasets": "5.0.1",
|
| 5 |
+
"huggingface_hub": "1.30.0",
|
| 6 |
+
"kernels": "0.16.1",
|
| 7 |
+
"accelerate": "1.14.0",
|
| 8 |
+
"numpy": "2.5.2",
|
| 9 |
+
"scikit-learn": "1.9.0"
|
| 10 |
+
}
|
eval_results.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
gate.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Serialize exactly the information available to an approve/deny classifier."""
|
| 2 |
+
def build_input(user_request, history, call):
|
| 3 |
+
parts = ['### PROPOSED TOOL CALL', f"tool: {call['tool']}", f"args: {call['args']}", '',
|
| 4 |
+
'### USER REQUEST', user_request, '', '### AGENT HISTORY']
|
| 5 |
+
if not history:
|
| 6 |
+
parts.append('(no prior actions)')
|
| 7 |
+
else:
|
| 8 |
+
for i, h in enumerate(history):
|
| 9 |
+
parts.append(f"[{i+1}] {h['tool']}({h['args']})\n-> {h.get('result', '')}")
|
| 10 |
+
return '\n'.join(parts)
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:10a27866e72a8fd7af878eb3d41e68f2de8e0f025cc20e55ef531554d1abc626
|
| 3 |
+
size 791684972
|
source_revisions.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"student": {
|
| 3 |
+
"repo": "ProCreations/auto-0.4b",
|
| 4 |
+
"revision": "2f6ec6ca79d733fa8c1677676a77cc85b739f0d2",
|
| 5 |
+
"kind": "model"
|
| 6 |
+
},
|
| 7 |
+
"teacher": {
|
| 8 |
+
"repo": "ProCreations/auto-1b-bf16",
|
| 9 |
+
"revision": "328c2d93e0390610bbeab4dea0000d0670a51be2",
|
| 10 |
+
"kind": "model"
|
| 11 |
+
},
|
| 12 |
+
"data": {
|
| 13 |
+
"repo": "ProCreations/auto-1b-data",
|
| 14 |
+
"revision": "d265bbf7bac76cd55daaf81a1d185dfe089a412b",
|
| 15 |
+
"kind": "dataset"
|
| 16 |
+
},
|
| 17 |
+
"benchmark": {
|
| 18 |
+
"repo": "ProCreations/approve-or-deny",
|
| 19 |
+
"revision": "a38b625913dd46ca9702063f1597c979e6ace34e",
|
| 20 |
+
"kind": "dataset"
|
| 21 |
+
}
|
| 22 |
+
}
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"clean_up_tokenization_spaces": true,
|
| 4 |
+
"cls_token": "[CLS]",
|
| 5 |
+
"is_local": true,
|
| 6 |
+
"local_files_only": false,
|
| 7 |
+
"mask_token": "[MASK]",
|
| 8 |
+
"max_length": 65536,
|
| 9 |
+
"model_input_names": [
|
| 10 |
+
"input_ids",
|
| 11 |
+
"attention_mask"
|
| 12 |
+
],
|
| 13 |
+
"model_max_length": 65536,
|
| 14 |
+
"pad_to_multiple_of": null,
|
| 15 |
+
"pad_token": "[PAD]",
|
| 16 |
+
"pad_token_type_id": 0,
|
| 17 |
+
"padding_side": "right",
|
| 18 |
+
"sep_token": "[SEP]",
|
| 19 |
+
"stride": 0,
|
| 20 |
+
"tokenizer_class": "TokenizersBackend",
|
| 21 |
+
"truncation_side": "right",
|
| 22 |
+
"truncation_strategy": "longest_first",
|
| 23 |
+
"unk_token": "[UNK]"
|
| 24 |
+
}
|
training/download.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import concurrent.futures, json, pathlib
|
| 2 |
+
from huggingface_hub import snapshot_download
|
| 3 |
+
|
| 4 |
+
ROOT = pathlib.Path('/home/user/.local/share/rtx-pro-apps/auto-0.4b-2')
|
| 5 |
+
SOURCES = {
|
| 6 |
+
'student': ('ProCreations/auto-0.4b', 'model', '2f6ec6ca79d733fa8c1677676a77cc85b739f0d2'),
|
| 7 |
+
'teacher': ('ProCreations/auto-1b-bf16', 'model', '328c2d93e0390610bbeab4dea0000d0670a51be2'),
|
| 8 |
+
'data': ('ProCreations/auto-1b-data', 'dataset', 'd265bbf7bac76cd55daaf81a1d185dfe089a412b'),
|
| 9 |
+
'benchmark': ('ProCreations/approve-or-deny', 'dataset', 'a38b625913dd46ca9702063f1597c979e6ace34e'),
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
def get(item):
|
| 13 |
+
name, (repo, kind, rev) = item
|
| 14 |
+
path = snapshot_download(repo, repo_type=kind, revision=rev, max_workers=4)
|
| 15 |
+
print(json.dumps({'downloaded': name, 'path': path}), flush=True)
|
| 16 |
+
return name, {'repo': repo, 'kind': kind, 'revision': rev, 'path': path}
|
| 17 |
+
|
| 18 |
+
if __name__ == '__main__':
|
| 19 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
|
| 20 |
+
manifest = dict(pool.map(get, SOURCES.items()))
|
| 21 |
+
(ROOT / 'sources.json').write_text(json.dumps(manifest, indent=2))
|
| 22 |
+
import pyarrow.parquet as pq
|
| 23 |
+
for name, filename in [('data', 'train.parquet'), ('data', 'validation.parquet'), ('benchmark', 'test.parquet')]:
|
| 24 |
+
f = pq.ParquetFile(pathlib.Path(manifest[name]['path']) / filename)
|
| 25 |
+
print(name, filename, f.metadata.num_rows, f.schema_arrow, flush=True)
|
| 26 |
+
row = next(f.iter_batches(batch_size=1)).to_pylist()[0]
|
| 27 |
+
print(json.dumps({k: str(v)[:200] for k, v in row.items()}, ensure_ascii=False), flush=True)
|
training/engine.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reproducible full-parameter ModernBERT fine-tuning and binary logit distillation."""
|
| 2 |
+
import os
|
| 3 |
+
os.environ.setdefault('TOKENIZERS_PARALLELISM', 'false')
|
| 4 |
+
os.environ.setdefault('OMP_NUM_THREADS', '8')
|
| 5 |
+
import gc, hashlib, json, math, pathlib, random, shutil, signal, time
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from datasets import load_from_disk
|
| 10 |
+
from scipy.special import softmax
|
| 11 |
+
from sklearn.metrics import accuracy_score, balanced_accuracy_score, f1_score, roc_auc_score, log_loss
|
| 12 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 13 |
+
from torch.utils.data import DataLoader
|
| 14 |
+
|
| 15 |
+
ROOT = pathlib.Path('/home/user/.local/share/rtx-pro-apps/auto-0.4b-2')
|
| 16 |
+
DATA = pathlib.Path('/home/user/datasets/auto-0.4b-2')
|
| 17 |
+
CKPT = pathlib.Path('/home/user/checkpoints/auto-0.4b-2')
|
| 18 |
+
LOG = pathlib.Path('/home/user/logs/auto-0.4b-2')
|
| 19 |
+
ATTENTION = 'kernels-community/flash-attn2@81fb77c12b2ad5d69380669b46739d5868614502'
|
| 20 |
+
SEED = 20260908
|
| 21 |
+
STOP = False
|
| 22 |
+
torch.set_num_threads(8)
|
| 23 |
+
torch.set_float32_matmul_precision('high')
|
| 24 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 25 |
+
|
| 26 |
+
def atomic_json(path, value):
|
| 27 |
+
path = pathlib.Path(path)
|
| 28 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 29 |
+
tmp = path.with_suffix(path.suffix + '.tmp')
|
| 30 |
+
tmp.write_text(json.dumps(value, indent=2, allow_nan=False))
|
| 31 |
+
tmp.replace(path)
|
| 32 |
+
|
| 33 |
+
def event(kind, **kwargs):
|
| 34 |
+
item = {'event': kind, 'time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), **kwargs}
|
| 35 |
+
print(json.dumps(item, allow_nan=False), flush=True)
|
| 36 |
+
LOG.mkdir(exist_ok=True, parents=True)
|
| 37 |
+
with (LOG / 'events.jsonl').open('a') as f:
|
| 38 |
+
f.write(json.dumps(item, allow_nan=False) + '\n')
|
| 39 |
+
atomic_json(ROOT / 'status.json', item)
|
| 40 |
+
|
| 41 |
+
def seed_all(seed=SEED):
|
| 42 |
+
random.seed(seed); np.random.seed(seed); torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)
|
| 43 |
+
|
| 44 |
+
def load_model(path, train=False):
|
| 45 |
+
model = AutoModelForSequenceClassification.from_pretrained(
|
| 46 |
+
str(path), dtype=torch.float32 if train else torch.bfloat16, attn_implementation=ATTENTION).cuda()
|
| 47 |
+
assert model.config.id2label == {0: 'approve', 1: 'deny'}
|
| 48 |
+
assert model.config.max_position_embeddings == 65536
|
| 49 |
+
model.train(train)
|
| 50 |
+
return model
|
| 51 |
+
|
| 52 |
+
def release(model):
|
| 53 |
+
del model
|
| 54 |
+
gc.collect(); torch.cuda.empty_cache()
|
| 55 |
+
|
| 56 |
+
def metrics(labels, logits, threshold=0.5):
|
| 57 |
+
labels = np.asarray(labels)
|
| 58 |
+
probs = softmax(np.asarray(logits, dtype=np.float64), axis=1)[:, 1]
|
| 59 |
+
pred = probs >= threshold
|
| 60 |
+
deny, approve = labels == 1, labels == 0
|
| 61 |
+
recalls = ([float(pred[deny].mean())] if deny.any() else []) + ([float((~pred[approve]).mean())] if approve.any() else [])
|
| 62 |
+
out = {'n': len(labels), 'accuracy': float(accuracy_score(labels, pred)),
|
| 63 |
+
'balanced_accuracy': float(np.mean(recalls)),
|
| 64 |
+
'f1_deny': float(f1_score(labels, pred, zero_division=0)),
|
| 65 |
+
'auroc': float(roc_auc_score(labels, probs)) if deny.any() and approve.any() else None,
|
| 66 |
+
'nll': float(log_loss(labels, np.stack([1-probs, probs], axis=1), labels=[0, 1])),
|
| 67 |
+
'false_approve_rate': float((~pred[deny]).mean()) if deny.any() else None,
|
| 68 |
+
'false_deny_rate': float(pred[approve].mean()) if approve.any() else None,
|
| 69 |
+
'false_approve_count': int((~pred[deny]).sum()), 'false_deny_count': int(pred[approve].sum()),
|
| 70 |
+
'deny_count': int(deny.sum()), 'approve_count': int(approve.sum()), 'threshold': float(threshold),
|
| 71 |
+
'brier': float(np.mean((probs-labels)**2))}
|
| 72 |
+
# Wilson binomial interval for accuracy.
|
| 73 |
+
p, n, z = out['accuracy'], len(labels), 1.959963984540054
|
| 74 |
+
center = (p + z*z/(2*n)) / (1+z*z/n)
|
| 75 |
+
half = z * math.sqrt(p*(1-p)/n + z*z/(4*n*n)) / (1+z*z/n)
|
| 76 |
+
out['accuracy_ci95'] = [center-half, center+half]
|
| 77 |
+
return out
|
| 78 |
+
|
| 79 |
+
def report(ds, indices, logits, threshold=0.5):
|
| 80 |
+
sub = ds.select([int(i) for i in indices])
|
| 81 |
+
labels = np.array(sub['labels'])
|
| 82 |
+
out = {'overall': metrics(labels, logits, threshold), 'slices': {}}
|
| 83 |
+
lens = np.array(sub['length'])
|
| 84 |
+
buckets = np.where(lens < 1024, '<1k', np.where(lens < 4096, '1k-4k', np.where(lens < 16384, '4k-16k', '16k-64k')))
|
| 85 |
+
for field, values in [('length', buckets), ('category', np.array(sub['category'])),
|
| 86 |
+
('difficulty', np.array(sub['difficulty'])), ('lang', np.array(sub['lang']))]:
|
| 87 |
+
out['slices'][field] = {str(v): metrics(labels[values == v], logits[values == v], threshold) for v in np.unique(values)}
|
| 88 |
+
return out
|
| 89 |
+
|
| 90 |
+
def collate(rows):
|
| 91 |
+
length = ((max(len(r['input_ids']) for r in rows)+7)//8)*8
|
| 92 |
+
ids = torch.full((len(rows), length), 50283, dtype=torch.long)
|
| 93 |
+
mask = torch.zeros_like(ids)
|
| 94 |
+
for i, row in enumerate(rows):
|
| 95 |
+
n = len(row['input_ids']); ids[i, :n] = torch.tensor(row['input_ids']); mask[i, :n] = 1
|
| 96 |
+
return {'input_ids': ids, 'attention_mask': mask, 'labels': torch.tensor([r['labels'] for r in rows], dtype=torch.long)}
|
| 97 |
+
|
| 98 |
+
def batches(indices, lengths, token_budget=16384, max_batch=32, seed=None):
|
| 99 |
+
indices = np.asarray(indices, dtype=np.int64).copy()
|
| 100 |
+
rng = np.random.default_rng(seed)
|
| 101 |
+
if seed is None:
|
| 102 |
+
indices = indices[np.argsort(lengths[indices], kind='stable')]
|
| 103 |
+
chunks = [indices]
|
| 104 |
+
else:
|
| 105 |
+
rng.shuffle(indices)
|
| 106 |
+
chunks = [c[np.argsort(lengths[c], kind='stable')] for c in np.array_split(indices, max(1, math.ceil(len(indices)/2048)))]
|
| 107 |
+
result = []
|
| 108 |
+
for chunk in chunks:
|
| 109 |
+
batch, maxlen = [], 0
|
| 110 |
+
for idx in chunk:
|
| 111 |
+
n = int(lengths[idx])
|
| 112 |
+
if batch and ((len(batch)+1) * max(maxlen, n) > token_budget or len(batch) >= max_batch):
|
| 113 |
+
result.append(batch); batch, maxlen = [], 0
|
| 114 |
+
batch.append(int(idx)); maxlen = max(maxlen, n)
|
| 115 |
+
if batch: result.append(batch)
|
| 116 |
+
if seed is not None: rng.shuffle(result)
|
| 117 |
+
return result
|
| 118 |
+
|
| 119 |
+
def optimizer_groups(microbatches, lengths, examples=128, tokens=131072):
|
| 120 |
+
groups, group, count, total = [], [], 0, 0
|
| 121 |
+
for batch in microbatches:
|
| 122 |
+
group.append(batch); count += len(batch); total += int(lengths[batch].sum())
|
| 123 |
+
if count >= examples or total >= tokens:
|
| 124 |
+
groups.append(group); group, count, total = [], 0, 0
|
| 125 |
+
if group: groups.append(group)
|
| 126 |
+
return groups
|
| 127 |
+
|
| 128 |
+
@torch.inference_mode()
|
| 129 |
+
def predict(model, ds, indices, name, output=None, token_budget=32768):
|
| 130 |
+
indices = np.asarray(indices, dtype=np.int64)
|
| 131 |
+
if output is not None and pathlib.Path(output).exists():
|
| 132 |
+
saved = np.load(output)
|
| 133 |
+
assert np.array_equal(saved['indices'], indices), 'Prediction cache index mismatch'
|
| 134 |
+
return saved['logits']
|
| 135 |
+
lengths = np.array(ds['length'])
|
| 136 |
+
bs = batches(indices, lengths, token_budget=token_budget, max_batch=64)
|
| 137 |
+
loader = DataLoader(ds, batch_sampler=bs, collate_fn=collate, num_workers=2, pin_memory=True)
|
| 138 |
+
logits = np.full((len(ds), 2), np.nan, dtype=np.float32)
|
| 139 |
+
was_training = model.training; model.eval()
|
| 140 |
+
start, done, last = time.monotonic(), 0, 0
|
| 141 |
+
for batch_indices, batch in zip(bs, loader):
|
| 142 |
+
x = {k: v.cuda(non_blocking=True) for k, v in batch.items() if k != 'labels'}
|
| 143 |
+
with torch.autocast('cuda', dtype=torch.bfloat16):
|
| 144 |
+
pred = model(**x).logits.float().cpu().numpy()
|
| 145 |
+
if not np.isfinite(pred).all(): raise RuntimeError('Nonfinite evaluation logits')
|
| 146 |
+
logits[batch_indices] = pred; done += len(batch_indices)
|
| 147 |
+
if time.monotonic()-last > 45:
|
| 148 |
+
event('evaluating', name=name, done=done, total=len(indices), elapsed_seconds=time.monotonic()-start)
|
| 149 |
+
last = time.monotonic()
|
| 150 |
+
model.train(was_training)
|
| 151 |
+
result = logits[indices]
|
| 152 |
+
assert np.isfinite(result).all()
|
| 153 |
+
if output is not None:
|
| 154 |
+
pathlib.Path(output).parent.mkdir(parents=True, exist_ok=True)
|
| 155 |
+
temp = str(output) + '.tmp.npz'; np.savez(temp, indices=indices, logits=result); os.replace(temp, output)
|
| 156 |
+
event('evaluation_complete', name=name, n=len(indices), elapsed_seconds=time.monotonic()-start)
|
| 157 |
+
return result
|
| 158 |
+
|
| 159 |
+
def selection_score(r):
|
| 160 |
+
short_and_long = r['overall']['balanced_accuracy']
|
| 161 |
+
long = r['slices']['length'].get('16k-64k', {}).get('balanced_accuracy', short_and_long)
|
| 162 |
+
return 0.7 * short_and_long + 0.3 * long
|
| 163 |
+
|
| 164 |
+
def export_model(model, path, tokenizer):
|
| 165 |
+
path = pathlib.Path(path)
|
| 166 |
+
staging = path.with_name(path.name + '.staging')
|
| 167 |
+
backup = path.with_name(path.name + '.previous')
|
| 168 |
+
if staging.exists(): shutil.rmtree(staging)
|
| 169 |
+
staging.mkdir(parents=True, exist_ok=True)
|
| 170 |
+
state = {k: v.detach().cpu().to(torch.bfloat16) if v.is_floating_point() else v.detach().cpu() for k,v in model.state_dict().items()}
|
| 171 |
+
old_dtype = model.config.dtype
|
| 172 |
+
model.config.dtype = torch.bfloat16
|
| 173 |
+
model.save_pretrained(str(staging), state_dict=state, safe_serialization=True)
|
| 174 |
+
model.config.dtype = old_dtype
|
| 175 |
+
tokenizer.save_pretrained(str(staging))
|
| 176 |
+
if backup.exists(): shutil.rmtree(backup)
|
| 177 |
+
if path.exists(): path.rename(backup)
|
| 178 |
+
staging.rename(path)
|
| 179 |
+
if backup.exists(): shutil.rmtree(backup)
|
| 180 |
+
del state
|
| 181 |
+
|
| 182 |
+
def save_resume(model, optimizer, scheduler, path, **progress):
|
| 183 |
+
state = {'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'scheduler': scheduler.state_dict(),
|
| 184 |
+
'rng_torch': torch.get_rng_state(), 'rng_cuda': torch.cuda.get_rng_state_all(),
|
| 185 |
+
'rng_numpy': np.random.get_state(), 'rng_python': random.getstate(), **progress}
|
| 186 |
+
tmp = str(path) + '.tmp'; torch.save(state, tmp); os.replace(tmp, path)
|
| 187 |
+
|
| 188 |
+
def stop_handler(*_):
|
| 189 |
+
global STOP
|
| 190 |
+
STOP = True
|
| 191 |
+
|
| 192 |
+
def train_phase(name, start_path, indices, epochs, lr, teacher_logits=None, alpha=0.5, temperature=2.0):
|
| 193 |
+
phase = CKPT / name; phase.mkdir(parents=True, exist_ok=True)
|
| 194 |
+
done_path = phase / 'complete.json'
|
| 195 |
+
if done_path.exists(): return json.loads(done_path.read_text())['best_path']
|
| 196 |
+
seed_all()
|
| 197 |
+
train = load_from_disk(str(DATA / 'train')); val = load_from_disk(str(DATA / 'validation'))
|
| 198 |
+
lengths = np.array(train['length'])
|
| 199 |
+
monitor = np.load(DATA / 'validation_partitions.npz')['monitor']
|
| 200 |
+
tokenizer = AutoTokenizer.from_pretrained(str(start_path))
|
| 201 |
+
model = load_model(start_path, train=True)
|
| 202 |
+
params = [p for p in model.parameters() if p.requires_grad]
|
| 203 |
+
optimizer = torch.optim.AdamW(params, lr=lr, betas=(0.9, 0.95), eps=1e-8, weight_decay=0.01, fused=True)
|
| 204 |
+
all_groups = [optimizer_groups(batches(indices, lengths, seed=SEED+e), lengths) for e in range(epochs)]
|
| 205 |
+
total_steps = sum(map(len, all_groups)); warmup = max(20, int(total_steps*0.03))
|
| 206 |
+
def schedule(step):
|
| 207 |
+
if step < warmup: return (step+1)/warmup
|
| 208 |
+
fraction = min(1., (step-warmup)/max(1,total_steps-warmup))
|
| 209 |
+
return 0.1 + 0.9*0.5*(1+math.cos(math.pi*fraction))
|
| 210 |
+
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, schedule)
|
| 211 |
+
start_epoch = start_group = global_step = 0; best_score = -1.; best_path = str(phase/'best')
|
| 212 |
+
resume_path = phase / 'resume.pt'
|
| 213 |
+
if resume_path.exists():
|
| 214 |
+
resume = torch.load(resume_path, map_location='cpu', weights_only=False)
|
| 215 |
+
model.load_state_dict(resume['model']); optimizer.load_state_dict(resume['optimizer']); scheduler.load_state_dict(resume['scheduler'])
|
| 216 |
+
start_epoch, start_group, global_step = resume['epoch'], resume['next_group'], resume['step']
|
| 217 |
+
best_score = resume['best_score']
|
| 218 |
+
if (phase/'best_metrics.json').exists():
|
| 219 |
+
best_score = max(best_score, json.loads((phase/'best_metrics.json').read_text())['score'])
|
| 220 |
+
torch.set_rng_state(resume['rng_torch']); torch.cuda.set_rng_state_all(resume['rng_cuda'])
|
| 221 |
+
np.random.set_state(resume['rng_numpy']); random.setstate(resume['rng_python'])
|
| 222 |
+
del resume
|
| 223 |
+
event('resumed', phase=name, epoch=start_epoch, group=start_group, step=global_step)
|
| 224 |
+
else:
|
| 225 |
+
initial = predict(model, val, monitor, name+'-initial')
|
| 226 |
+
initial_report = report(val, monitor, initial); best_score = selection_score(initial_report)
|
| 227 |
+
export_model(model, best_path, tokenizer)
|
| 228 |
+
atomic_json(phase/'best_metrics.json', {'score': best_score, 'step': 0, 'report': initial_report})
|
| 229 |
+
teacher = np.load(teacher_logits, mmap_mode='r') if teacher_logits else None
|
| 230 |
+
if teacher is not None: assert teacher.shape == (len(train), 2) and np.isfinite(teacher).all()
|
| 231 |
+
event('phase_started', phase=name, rows=len(indices), epochs=epochs, lr=lr, total_steps=total_steps, distillation=teacher is not None)
|
| 232 |
+
last_log = last_save = time.monotonic(); started = last_log; seen = tokens_seen = 0; loss_sum = 0.; loss_examples = 0
|
| 233 |
+
ckpt_enabled = False
|
| 234 |
+
eval_interval = max(100, math.ceil(total_steps / (epochs*4)))
|
| 235 |
+
signal.signal(signal.SIGTERM, stop_handler); signal.signal(signal.SIGINT, stop_handler)
|
| 236 |
+
for epoch, groups in enumerate(all_groups):
|
| 237 |
+
if epoch < start_epoch: continue
|
| 238 |
+
begin = start_group if epoch == start_epoch else 0
|
| 239 |
+
# Background CPU collation, in exact deterministic group order.
|
| 240 |
+
remaining_batches = [b for group in groups[begin:] for b in group]
|
| 241 |
+
loader = iter(DataLoader(train, batch_sampler=remaining_batches, collate_fn=collate, num_workers=4, pin_memory=True, prefetch_factor=2))
|
| 242 |
+
for group_idx in range(begin, len(groups)):
|
| 243 |
+
group = groups[group_idx]; n_group = sum(map(len, group)); optimizer.zero_grad(set_to_none=True)
|
| 244 |
+
for batch_indices in group:
|
| 245 |
+
batch = next(loader)
|
| 246 |
+
want_checkpoint = batch['input_ids'].shape[1] > 8192
|
| 247 |
+
if want_checkpoint != ckpt_enabled:
|
| 248 |
+
if want_checkpoint: model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={'use_reentrant': False})
|
| 249 |
+
else: model.gradient_checkpointing_disable()
|
| 250 |
+
ckpt_enabled = want_checkpoint
|
| 251 |
+
x = {k: v.cuda(non_blocking=True) for k,v in batch.items()}
|
| 252 |
+
labels = x.pop('labels')
|
| 253 |
+
with torch.autocast('cuda', dtype=torch.bfloat16):
|
| 254 |
+
logits = model(**x).logits.float()
|
| 255 |
+
ce = F.cross_entropy(logits, labels, reduction='none')
|
| 256 |
+
if teacher is not None:
|
| 257 |
+
tl = torch.tensor(np.array(teacher[batch_indices]), device='cuda', dtype=torch.float32)
|
| 258 |
+
target = F.softmax(tl/temperature, dim=-1)
|
| 259 |
+
kl = F.kl_div(F.log_softmax(logits/temperature, dim=-1), target, reduction='none').sum(-1)*temperature**2
|
| 260 |
+
# Preserve hard-label evidence on teacher mistakes while still learning soft uncertainty.
|
| 261 |
+
weight = torch.where(tl.argmax(-1) == labels, alpha, alpha*0.25)
|
| 262 |
+
per_example = (1-weight)*ce + weight*kl
|
| 263 |
+
else: per_example = ce
|
| 264 |
+
loss = per_example.sum()/n_group
|
| 265 |
+
if not torch.isfinite(loss): raise RuntimeError('Nonfinite training loss')
|
| 266 |
+
loss.backward()
|
| 267 |
+
loss_sum += float(per_example.detach().sum()); loss_examples += len(batch_indices)
|
| 268 |
+
seen += len(batch_indices); tokens_seen += int(lengths[batch_indices].sum())
|
| 269 |
+
del logits, loss, per_example, ce, x, labels
|
| 270 |
+
grad_norm = torch.nn.utils.clip_grad_norm_(params, 1.0, error_if_nonfinite=True)
|
| 271 |
+
optimizer.step(); scheduler.step(); global_step += 1
|
| 272 |
+
now = time.monotonic()
|
| 273 |
+
if now-last_log > 45 or global_step % 100 == 0:
|
| 274 |
+
event('training', phase=name, epoch=epoch+1, step=global_step, total_steps=total_steps,
|
| 275 |
+
loss=loss_sum/max(1,loss_examples), grad_norm=float(grad_norm), lr=scheduler.get_last_lr()[0],
|
| 276 |
+
examples_this_run=seen, tokens_per_second=tokens_seen/max(1,now-started), elapsed_seconds=now-started)
|
| 277 |
+
loss_sum = 0.; loss_examples = 0; last_log=now
|
| 278 |
+
if global_step % eval_interval == 0 or group_idx == len(groups)-1:
|
| 279 |
+
pred = predict(model, val, monitor, name+f'-step{global_step}')
|
| 280 |
+
r = report(val, monitor, pred); score=selection_score(r)
|
| 281 |
+
atomic_json(phase/f'monitor-{global_step}.json', r)
|
| 282 |
+
event('validation', phase=name, step=global_step, score=score, metrics=r['overall'], long_metrics=r['slices']['length'].get('16k-64k'))
|
| 283 |
+
if score > best_score:
|
| 284 |
+
best_score = score; export_model(model, best_path, tokenizer)
|
| 285 |
+
atomic_json(phase/'best_metrics.json', {'score':score,'step':global_step,'report':r})
|
| 286 |
+
if now-last_save > 600 or STOP or group_idx == len(groups)-1:
|
| 287 |
+
save_resume(model, optimizer, scheduler, resume_path, epoch=epoch, next_group=group_idx+1, step=global_step, best_score=best_score)
|
| 288 |
+
last_save = time.monotonic()
|
| 289 |
+
if STOP:
|
| 290 |
+
event('stopped_safely', phase=name, step=global_step)
|
| 291 |
+
raise SystemExit(75)
|
| 292 |
+
export_model(model, phase/'last', tokenizer)
|
| 293 |
+
atomic_json(done_path, {'best_path':best_path,'score':best_score,'steps':global_step,'last_path':str(phase/'last')})
|
| 294 |
+
event('phase_complete', phase=name, steps=global_step, best_score=best_score)
|
| 295 |
+
del optimizer, scheduler, params, model
|
| 296 |
+
gc.collect(); torch.cuda.empty_cache()
|
| 297 |
+
return best_path
|
| 298 |
+
|
| 299 |
+
def baseline():
|
| 300 |
+
sources = json.loads((ROOT/'sources.json').read_text())
|
| 301 |
+
ds = load_from_disk(str(DATA/'benchmark')); val = load_from_disk(str(DATA/'validation'))
|
| 302 |
+
selection = np.load(DATA/'validation_partitions.npz')['selection']
|
| 303 |
+
out = CKPT/'baseline'; out.mkdir(exist_ok=True, parents=True)
|
| 304 |
+
student_tok=AutoTokenizer.from_pretrained(sources['student']['path']); teacher_tok=AutoTokenizer.from_pretrained(sources['teacher']['path'])
|
| 305 |
+
assert student_tok.get_vocab() == teacher_tok.get_vocab()
|
| 306 |
+
assert student_tok('unicode Ω 中文 test')['input_ids'] == teacher_tok('unicode Ω 中文 test')['input_ids']
|
| 307 |
+
for name in ['student','teacher']:
|
| 308 |
+
if (out/f'{name}.json').exists(): continue
|
| 309 |
+
model=load_model(sources[name]['path'])
|
| 310 |
+
logits=predict(model,ds,np.arange(len(ds)),name+'-benchmark',out/f'{name}-benchmark.npz')
|
| 311 |
+
r={'benchmark':report(ds,np.arange(len(ds)),logits)}
|
| 312 |
+
logits=predict(model,val,selection,name+'-validation',out/f'{name}-selection.npz')
|
| 313 |
+
r['selection']=report(val,selection,logits)
|
| 314 |
+
atomic_json(out/f'{name}.json',r)
|
| 315 |
+
event('baseline_complete',model=name,benchmark=r['benchmark']['overall'],selection=r['selection']['overall'])
|
| 316 |
+
del model; gc.collect(); torch.cuda.empty_cache()
|
| 317 |
+
|
| 318 |
+
if __name__ == '__main__':
|
| 319 |
+
import sys
|
| 320 |
+
if sys.argv[1] == 'baseline': baseline()
|
training/prepare.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pinned data preparation; no benchmark labels are used to build training data."""
|
| 2 |
+
import os
|
| 3 |
+
os.environ['TOKENIZERS_PARALLELISM'] = 'false'
|
| 4 |
+
import collections, hashlib, json, pathlib, re
|
| 5 |
+
import numpy as np
|
| 6 |
+
from datasets import Dataset, load_from_disk
|
| 7 |
+
from transformers import AutoTokenizer
|
| 8 |
+
|
| 9 |
+
ROOT = pathlib.Path('/home/user/.local/share/rtx-pro-apps/auto-0.4b-2')
|
| 10 |
+
DATA = pathlib.Path('/home/user/datasets/auto-0.4b-2')
|
| 11 |
+
SOURCES = json.loads((ROOT / 'sources.json').read_text())
|
| 12 |
+
TOKENIZER = None
|
| 13 |
+
KEEP = ['label', 'category', 'difficulty', 'lang', 'is_long', 'needle_depth', 'subset', 'text_hash', 'group_hash', 'source_row']
|
| 14 |
+
|
| 15 |
+
def digest(s):
|
| 16 |
+
return hashlib.sha256(re.sub(r'\s+', ' ', s).strip().encode()).hexdigest()
|
| 17 |
+
|
| 18 |
+
def fingerprints(batch, indices):
|
| 19 |
+
groups = []
|
| 20 |
+
for req, call in zip(batch['user_request'], batch['call']):
|
| 21 |
+
try:
|
| 22 |
+
call = json.dumps(json.loads(call), sort_keys=True, ensure_ascii=False)
|
| 23 |
+
except (ValueError, TypeError):
|
| 24 |
+
pass
|
| 25 |
+
groups.append(digest(req + '\n' + call))
|
| 26 |
+
return {'text_hash': [digest(t) for t in batch['text']], 'group_hash': groups, 'source_row': indices}
|
| 27 |
+
|
| 28 |
+
def tokenize(batch):
|
| 29 |
+
global TOKENIZER
|
| 30 |
+
if TOKENIZER is None:
|
| 31 |
+
TOKENIZER = AutoTokenizer.from_pretrained(SOURCES['student']['path'])
|
| 32 |
+
ids = TOKENIZER(batch['text'], truncation=False, return_attention_mask=False)['input_ids']
|
| 33 |
+
original = [len(x) for x in ids]
|
| 34 |
+
# Model limit only. Short-context stages exclude long rows instead of corrupting their labels by truncation.
|
| 35 |
+
ids = [x if len(x) <= 65536 else x[:65535] + [TOKENIZER.sep_token_id] for x in ids]
|
| 36 |
+
return {'input_ids': ids, 'length': [len(x) for x in ids], 'original_length': original,
|
| 37 |
+
'labels': [0 if x == 'approve' else 1 for x in batch['label']]}
|
| 38 |
+
|
| 39 |
+
def load_raw(name, filename):
|
| 40 |
+
ds = Dataset.from_parquet(str(pathlib.Path(SOURCES[name]['path']) / filename), cache_dir=str(DATA / 'cache'))
|
| 41 |
+
assert set(ds.unique('label')) == {'approve', 'deny'}
|
| 42 |
+
return ds.map(fingerprints, batched=True, batch_size=256, with_indices=True, num_proc=8, desc='Fingerprint ' + filename)
|
| 43 |
+
|
| 44 |
+
def save_tokens(ds, name):
|
| 45 |
+
path = DATA / name
|
| 46 |
+
if path.exists():
|
| 47 |
+
return load_from_disk(str(path))
|
| 48 |
+
ds = ds.map(tokenize, batched=True, batch_size=64, num_proc=8,
|
| 49 |
+
remove_columns=[k for k in ds.column_names if k not in KEEP], desc='Tokenize ' + name)
|
| 50 |
+
ds.save_to_disk(str(path), max_shard_size='512MB')
|
| 51 |
+
return ds
|
| 52 |
+
|
| 53 |
+
if __name__ == '__main__':
|
| 54 |
+
DATA.mkdir(parents=True, exist_ok=True)
|
| 55 |
+
audit = {'sources': {k: {a: v[a] for a in ['repo', 'revision']} for k, v in SOURCES.items()},
|
| 56 |
+
'deduplication': 'SHA256 whitespace-normalized text; request+canonical call group exclusion across splits; conflicting exact-text labels removed from train.',
|
| 57 |
+
'seed': 20260908}
|
| 58 |
+
bench = load_raw('benchmark', 'test.parquet')
|
| 59 |
+
bench_text, bench_groups = set(bench['text_hash']), set(bench['group_hash'])
|
| 60 |
+
audit['benchmark_rows'] = len(bench)
|
| 61 |
+
bench = save_tokens(bench, 'benchmark')
|
| 62 |
+
val = load_raw('data', 'validation.parquet')
|
| 63 |
+
vtexts, vgroups = list(val['text_hash']), list(val['group_hash'])
|
| 64 |
+
seen, keep = set(), []
|
| 65 |
+
for i, (t, g) in enumerate(zip(vtexts, vgroups)):
|
| 66 |
+
if t not in bench_text and g not in bench_groups and t not in seen:
|
| 67 |
+
keep.append(i)
|
| 68 |
+
seen.add(t)
|
| 69 |
+
audit['validation_original'] = len(val)
|
| 70 |
+
val = val.select(keep)
|
| 71 |
+
val_text, val_groups = set(val['text_hash']), set(val['group_hash'])
|
| 72 |
+
val = save_tokens(val, 'validation')
|
| 73 |
+
partition = np.array([int(h[:8], 16) % 10 for h in val['group_hash']])
|
| 74 |
+
selection = np.flatnonzero(partition < 6)
|
| 75 |
+
calibration = np.flatnonzero((partition >= 6) & (partition < 8))
|
| 76 |
+
audit_indices = np.flatnonzero(partition >= 8)
|
| 77 |
+
rng = np.random.default_rng(20260908)
|
| 78 |
+
# All long selection examples plus a fixed random short subset for stable long-context monitoring.
|
| 79 |
+
lengths = np.array(val['length'])
|
| 80 |
+
short_selection = selection[lengths[selection] <= 4096]
|
| 81 |
+
monitor = np.sort(np.concatenate([rng.choice(short_selection, min(1536, len(short_selection)), replace=False), selection[lengths[selection] > 4096]]))
|
| 82 |
+
np.savez(DATA / 'validation_partitions.npz', selection=selection, calibration=calibration, audit=audit_indices, monitor=monitor)
|
| 83 |
+
audit['validation_partitions'] = {k: len(v) for k, v in [('selection', selection), ('calibration', calibration), ('audit', audit_indices), ('monitor', monitor)]}
|
| 84 |
+
(DATA / 'eval_ready.json').write_text(json.dumps(audit, indent=2))
|
| 85 |
+
train = load_raw('data', 'train.parquet')
|
| 86 |
+
texts, groups, labels = list(train['text_hash']), list(train['group_hash']), list(train['label'])
|
| 87 |
+
by_text, conflicts = {}, set()
|
| 88 |
+
for t, y in zip(texts, labels):
|
| 89 |
+
if t in by_text and by_text[t] != y:
|
| 90 |
+
conflicts.add(t)
|
| 91 |
+
by_text[t] = y
|
| 92 |
+
seen, keep, removed = set(), [], collections.Counter()
|
| 93 |
+
for i, (t, g) in enumerate(zip(texts, groups)):
|
| 94 |
+
reason = ('benchmark_text_overlap' if t in bench_text else
|
| 95 |
+
'benchmark_group_overlap' if g in bench_groups else
|
| 96 |
+
'validation_text_overlap' if t in val_text else
|
| 97 |
+
'validation_group_overlap' if g in val_groups else
|
| 98 |
+
'conflicting_labels' if t in conflicts else
|
| 99 |
+
'duplicate_text' if t in seen else None)
|
| 100 |
+
if reason:
|
| 101 |
+
removed[reason] += 1
|
| 102 |
+
else:
|
| 103 |
+
keep.append(i)
|
| 104 |
+
seen.add(t)
|
| 105 |
+
audit['train_original'] = len(train)
|
| 106 |
+
audit['removed_train'] = dict(removed)
|
| 107 |
+
train = save_tokens(train.select(keep), 'train')
|
| 108 |
+
for name, ds in [('train', train), ('validation', val), ('benchmark', bench)]:
|
| 109 |
+
lengths = np.array(ds['length'])
|
| 110 |
+
audit[name] = {'rows': len(ds), 'tokens': int(lengths.sum()),
|
| 111 |
+
'label_counts': dict(collections.Counter(ds['label'])),
|
| 112 |
+
'length_percentiles': dict(zip(['p0','p25','p50','p75','p90','p95','p99','p100'], np.percentile(lengths, [0,25,50,75,90,95,99,100]).tolist())),
|
| 113 |
+
'over_model_limit': int(sum(n > 65536 for n in ds['original_length'])),
|
| 114 |
+
'gt4096': int((lengths > 4096).sum()), 'gt16384': int((lengths > 16384).sum())}
|
| 115 |
+
(DATA / 'data_audit.json').write_text(json.dumps(audit, indent=2))
|
| 116 |
+
print(json.dumps(audit, indent=2), flush=True)
|
training/publish.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Freeze on validation, audit once, publish only an improvement that meets declared quality gates."""
|
| 2 |
+
import gc, hashlib, json, pathlib, shutil, time
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from datasets import load_from_disk
|
| 6 |
+
from scipy.special import softmax
|
| 7 |
+
from scipy.stats import binomtest
|
| 8 |
+
from huggingface_hub import HfApi, snapshot_download
|
| 9 |
+
from huggingface_hub.errors import RepositoryNotFoundError
|
| 10 |
+
from engine import ROOT, DATA, CKPT, LOG, ATTENTION, SEED, atomic_json, event, load_model, predict, report, metrics, selection_score
|
| 11 |
+
|
| 12 |
+
REPO='ProCreations/auto-0.4b-2'
|
| 13 |
+
|
| 14 |
+
def paired_comparison(labels, original, final):
|
| 15 |
+
b=(original.argmax(-1)==labels); f=(final.argmax(-1)==labels)
|
| 16 |
+
difference=f.astype(float)-b.astype(float)
|
| 17 |
+
rng=np.random.default_rng(SEED)
|
| 18 |
+
means=np.array([rng.choice(difference,size=len(difference),replace=True).mean() for _ in range(10000)])
|
| 19 |
+
improved=int((f&~b).sum()); regressed=int((b&~f).sum())
|
| 20 |
+
return {'accuracy_gain':float(difference.mean()),'gain_ci95':np.quantile(means,[0.025,0.975]).tolist(),
|
| 21 |
+
'fixed_errors':improved,'new_errors':regressed,
|
| 22 |
+
'mcnemar_exact_p':float(binomtest(improved,improved+regressed,0.5).pvalue) if improved+regressed else 1.0}
|
| 23 |
+
|
| 24 |
+
def finalize(selected_sft=None):
|
| 25 |
+
from run import PLAN
|
| 26 |
+
val=load_from_disk(str(DATA/'validation'));bench=load_from_disk(str(DATA/'benchmark'))
|
| 27 |
+
parts=dict(np.load(DATA/'validation_partitions.npz'))
|
| 28 |
+
evaluated=CKPT/'final_evaluation';evaluated.mkdir(exist_ok=True,parents=True)
|
| 29 |
+
frozen_path=evaluated/'frozen_selection.json'
|
| 30 |
+
if frozen_path.exists(): frozen=json.loads(frozen_path.read_text())
|
| 31 |
+
elif selected_sft is not None:
|
| 32 |
+
frozen={'chosen':selected_sft,'selection_data':'Validation-only checkpoint selection; distillation skipped by the user-requested post-SFT benchmark comparison rule.'}
|
| 33 |
+
atomic_json(evaluated/'selection_candidates.json',[selected_sft])
|
| 34 |
+
atomic_json(frozen_path,frozen)
|
| 35 |
+
else:
|
| 36 |
+
candidates=[]
|
| 37 |
+
for phase in ['sft_short','sft_long','kd_short','kd_long']:
|
| 38 |
+
for which in ['best','last']:
|
| 39 |
+
path=CKPT/phase/which
|
| 40 |
+
model=load_model(path)
|
| 41 |
+
logits=predict(model,val,parts['selection'],phase+'-'+which+'-selection',evaluated/f'{phase}-{which}-selection.npz')
|
| 42 |
+
r=report(val,parts['selection'],logits)
|
| 43 |
+
kd_updates=0
|
| 44 |
+
if phase.startswith('kd_'):
|
| 45 |
+
progress=json.loads((CKPT/phase/('complete.json' if which=='last' else 'best_metrics.json')).read_text())
|
| 46 |
+
kd_updates=int(progress['steps'] if which=='last' else progress['step'])
|
| 47 |
+
if phase=='kd_long':kd_updates+=int(json.loads((CKPT/'kd_short/best_metrics.json').read_text())['step'])
|
| 48 |
+
candidates.append({'path':str(path),'name':phase+'-'+which,'score':selection_score(r),'distillation_optimizer_steps':kd_updates,'report':r})
|
| 49 |
+
del model;gc.collect();torch.cuda.empty_cache()
|
| 50 |
+
atomic_json(evaluated/'selection_candidates.json',candidates)
|
| 51 |
+
# Final release must have completed distillation. Compare KD candidates; retain SFT comparisons for the report.
|
| 52 |
+
eligible=[x for x in candidates if x['name'].startswith('kd_') and x['distillation_optimizer_steps']>0]
|
| 53 |
+
chosen=max(eligible,key=lambda x:(x['score'],-x['report']['overall']['nll']))
|
| 54 |
+
frozen={'chosen':chosen,'selection_data':'validation selection only; benchmark and validation audit excluded'}
|
| 55 |
+
atomic_json(frozen_path,frozen)
|
| 56 |
+
chosen=frozen['chosen'];model=load_model(chosen['path'])
|
| 57 |
+
cal_logits=predict(model,val,parts['calibration'],'final-calibration',evaluated/'calibration.npz')
|
| 58 |
+
cal_labels=np.array(val.select(parts['calibration'].tolist())['labels'])
|
| 59 |
+
# A separately documented operating point; raw model/default-threshold quality is what gates publication.
|
| 60 |
+
thresholds=np.arange(0.05,0.951,0.005)
|
| 61 |
+
opts=[metrics(cal_labels,cal_logits,float(t)) for t in thresholds]
|
| 62 |
+
operating=max(opts,key=lambda x:(x['balanced_accuracy'],-abs(x['threshold']-0.5)))['threshold']
|
| 63 |
+
atomic_json(evaluated/'calibration.json',{'recommended_threshold':operating,'selection':'maximum balanced accuracy; closest to 0.5 on ties','rows':len(cal_labels)})
|
| 64 |
+
audit_logits=predict(model,val,parts['audit'],'final-validation-audit',evaluated/'audit.npz')
|
| 65 |
+
final_logits=predict(model,bench,np.arange(len(bench)),'final-benchmark',evaluated/'benchmark.npz')
|
| 66 |
+
audit=report(val,parts['audit'],audit_logits)
|
| 67 |
+
benchmark=report(bench,np.arange(len(bench)),final_logits)
|
| 68 |
+
tuned=report(bench,np.arange(len(bench)),final_logits,operating)
|
| 69 |
+
baseline_student=json.loads((CKPT/'baseline/student.json').read_text())
|
| 70 |
+
baseline_teacher=json.loads((CKPT/'baseline/teacher.json').read_text())
|
| 71 |
+
original=np.load(CKPT/'baseline/student-benchmark.npz')['logits']
|
| 72 |
+
pair=paired_comparison(np.array(bench['labels']),original,final_logits)
|
| 73 |
+
b=baseline_student['benchmark']['overall'];f=benchmark['overall'];gate=PLAN['publish_gate']
|
| 74 |
+
checks={
|
| 75 |
+
'accuracy_at_least_95_percent':f['accuracy']>=gate['benchmark_accuracy_min'],
|
| 76 |
+
'long_context_at_least_90_percent':benchmark['slices']['length']['16k-64k']['accuracy']>=gate['benchmark_long_accuracy_min'],
|
| 77 |
+
'false_approve_improved':b['false_approve_rate']-f['false_approve_rate']>=gate['false_approve_improvement_min'],
|
| 78 |
+
'false_deny_improved':b['false_deny_rate']-f['false_deny_rate']>=gate['false_deny_improvement_min'],
|
| 79 |
+
'validation_audit_at_least_95_percent':audit['overall']['accuracy']>=gate['validation_audit_accuracy_min'],
|
| 80 |
+
'paired_improvement_ci_excludes_zero':pair['gain_ci95'][0]>gate['paired_accuracy_ci95_lower_min'],
|
| 81 |
+
}
|
| 82 |
+
result={'selected_checkpoint':chosen,'benchmark_default':benchmark,'benchmark_calibrated_threshold':tuned,
|
| 83 |
+
'validation_audit':audit,'recommended_threshold':operating,'paired_comparison':pair,
|
| 84 |
+
'baseline_student':baseline_student,'baseline_teacher':baseline_teacher,'quality_checks':checks,
|
| 85 |
+
'runtime_attention':ATTENTION,'benchmark_revision':json.loads((ROOT/'sources.json').read_text())['benchmark']['revision'],
|
| 86 |
+
'distillation_performed':selected_sft is None,
|
| 87 |
+
'post_sft_comparison':json.loads((ROOT/'post_sft_benchmark.json').read_text())}
|
| 88 |
+
atomic_json(evaluated/'results.json',result)
|
| 89 |
+
if not all(checks.values()):
|
| 90 |
+
event('quality_gate_failed',checks=checks,benchmark=f,audit=audit['overall'],results_path=str(evaluated/'results.json'))
|
| 91 |
+
raise SystemExit(20)
|
| 92 |
+
publish(model,chosen['path'],result,bench,final_logits)
|
| 93 |
+
|
| 94 |
+
def model_card(r,audit,plan):
|
| 95 |
+
b=r['baseline_student']['benchmark']['overall'];t=r['baseline_teacher']['benchmark']['overall'];f=r['benchmark_default']['overall']
|
| 96 |
+
long=r['benchmark_default']['slices']['length']['16k-64k'];gain=r['paired_comparison']
|
| 97 |
+
distilled=r['distillation_performed']
|
| 98 |
+
teacher_base=' - ProCreations/auto-1b-bf16\n' if distilled else ''
|
| 99 |
+
distillation_tag=' - distillation\n' if distilled else ''
|
| 100 |
+
method='followed by logit distillation from [auto-1b-bf16](https://huggingface.co/ProCreations/auto-1b-bf16)' if distilled else 'with supervised training on short and full-length long-context examples; teacher distillation was skipped because the post-SFT benchmark accuracy exceeded the teacher'
|
| 101 |
+
stages='then teacher logits over all training rows; one short distillation epoch and one long distillation epoch with replay' if distilled else 'teacher distillation was skipped under the user-requested benchmark comparison rule'
|
| 102 |
+
kd_detail=f"Distillation uses temperature {plan['kd_short']['temperature']} and a mixture of hard-label cross entropy and teacher KL divergence. Teacher disagreement reduces the KL weight so teacher errors do not replace the dataset labels." if distilled else 'No teacher distillation was applied to this release.'
|
| 103 |
+
intermediate=r['post_sft_comparison']['benchmark']['overall']['accuracy']
|
| 104 |
+
def row(name,m): return f"| {name} | {100*m['accuracy']:.2f}% | {100*m['false_approve_rate']:.2f}% | {100*m['false_deny_rate']:.2f}% | {m['auroc']:.4f} |"
|
| 105 |
+
return f'''---
|
| 106 |
+
license: apache-2.0
|
| 107 |
+
base_model:
|
| 108 |
+
- ProCreations/auto-0.4b
|
| 109 |
+
{teacher_base}pipeline_tag: text-classification
|
| 110 |
+
library_name: transformers
|
| 111 |
+
datasets:
|
| 112 |
+
- ProCreations/auto-1b-data
|
| 113 |
+
tags:
|
| 114 |
+
- modernbert
|
| 115 |
+
{distillation_tag} - agent-safety
|
| 116 |
+
- tool-calling
|
| 117 |
+
- long-context
|
| 118 |
+
model-index:
|
| 119 |
+
- name: auto-0.4b-2
|
| 120 |
+
results:
|
| 121 |
+
- task:
|
| 122 |
+
type: text-classification
|
| 123 |
+
name: Agentic tool-call approve/deny classification
|
| 124 |
+
dataset:
|
| 125 |
+
name: Approve-or-Deny
|
| 126 |
+
type: ProCreations/approve-or-deny
|
| 127 |
+
split: test
|
| 128 |
+
revision: {r['benchmark_revision']}
|
| 129 |
+
metrics:
|
| 130 |
+
- type: accuracy
|
| 131 |
+
value: {f['accuracy']}
|
| 132 |
+
- type: f1
|
| 133 |
+
value: {f['f1_deny']}
|
| 134 |
+
name: F1 (deny)
|
| 135 |
+
- type: roc_auc
|
| 136 |
+
value: {f['auroc']}
|
| 137 |
+
- type: false_approve_rate
|
| 138 |
+
value: {f['false_approve_rate']}
|
| 139 |
+
- type: false_deny_rate
|
| 140 |
+
value: {f['false_deny_rate']}
|
| 141 |
+
---
|
| 142 |
+
|
| 143 |
+
# auto-0.4b-2
|
| 144 |
+
|
| 145 |
+
A 395.8M-parameter ModernBERT encoder for deciding whether an agent's proposed tool call is authorized and safe in context. Continued full-parameter training of [auto-0.4b](https://huggingface.co/ProCreations/auto-0.4b), {method}, on one NVIDIA RTX PRO 6000 Blackwell 96 GB GPU.
|
| 146 |
+
|
| 147 |
+
## Measured results
|
| 148 |
+
|
| 149 |
+
The same pinned 3,000-item benchmark, input serialization, tokenizer, full input lengths, attention implementation, and default `P(deny) >= 0.5` threshold were used for all three models below. Original and teacher results are fresh measurements; older model-card scores can differ across data/runtime revisions.
|
| 150 |
+
|
| 151 |
+
| Model | Accuracy | False approve | False deny | AUROC |
|
| 152 |
+
|---|---:|---:|---:|---:|
|
| 153 |
+
{row('Original auto-0.4b',b)}
|
| 154 |
+
{row('Teacher auto-1b-bf16',t)}
|
| 155 |
+
{row('**auto-0.4b-2**',f)}
|
| 156 |
+
|
| 157 |
+
Accuracy improved by **{100*gain['accuracy_gain']:.2f} percentage points**; paired bootstrap 95% interval: {100*gain['gain_ci95'][0]:.2f} to {100*gain['gain_ci95'][1]:.2f} points. Accuracy Wilson 95% interval: {100*f['accuracy_ci95'][0]:.2f}%–{100*f['accuracy_ci95'][1]:.2f}%.
|
| 158 |
+
|
| 159 |
+
Accuracy on inputs of 16,384–65,536 tokens: **{100*long['accuracy']:.2f}%** ({long['n']} items). The untouched validation audit partition scored **{100*r['validation_audit']['overall']['accuracy']:.2f}%** across {r['validation_audit']['overall']['n']} items. Full category, difficulty, language, length, confusion counts, confidence intervals, and threshold results are in `eval_results.json`.
|
| 160 |
+
|
| 161 |
+
The separately calibrated recommended threshold is `{r['recommended_threshold']:.3f}`. It was chosen only on 2,581 validation calibration rows. The table above uses 0.5, without test-set threshold tuning.
|
| 162 |
+
|
| 163 |
+
## Training and evaluation integrity
|
| 164 |
+
|
| 165 |
+
- {audit['train']['rows']:,} retained training examples, {audit['train']['tokens']:,} tokens. Exact normalized-text deduplication and request-plus-call group separation removed 15 rows. There was no detected benchmark overlap.
|
| 166 |
+
- 13,000 validation rows were split by request-plus-call group into 7,824 selection, 2,581 calibration, and 2,595 untouched audit rows. Benchmark examples were excluded from training targets, teacher-target generation, threshold tuning, and checkpoint selection.
|
| 167 |
+
- At the user's request, a checkpoint frozen on validation was evaluated after long SFT, scoring {100*intermediate:.2f}% benchmark accuracy. This intermediate benchmark decided whether to run distillation: distill if the student did not strictly beat the teacher's {100*t['accuracy']:.2f}%; otherwise skip it. Consequently the benchmark was used for this training-procedure decision and is not an untouched one-use final test. The independent validation audit remained unused until final evaluation.
|
| 168 |
+
- Two short-context supervised epochs; one full-length long-context supervised epoch with short replay; {stages}.
|
| 169 |
+
- Every retained training row was used at full tokenized length. No training example required truncation. Configured capacity is 65,536 tokens; the supplied training corpus reaches 51,408 tokens and benchmark reaches 56,176 with this tokenizer. The 65,536-token path is a capacity smoke test, not a claim of measured accuracy at exactly 65,536 tokens.
|
| 170 |
+
- Full-parameter AdamW with FP32 master weights and moments, BF16 compute, cosine learning-rate schedules, gradient clipping, and activation checkpointing for long inputs. BF16 safetensors are published.
|
| 171 |
+
- {kd_detail}
|
| 172 |
+
- Each checkpoint was frozen using validation selection scores before its benchmark evaluation. Training sources and immutable revisions, data audit, plan, environment versions, and runnable scripts are included.
|
| 173 |
+
|
| 174 |
+
## Usage
|
| 175 |
+
|
| 176 |
+
```python
|
| 177 |
+
import torch
|
| 178 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 179 |
+
|
| 180 |
+
{(ROOT/'gate.py').read_text()}
|
| 181 |
+
|
| 182 |
+
repo = "ProCreations/auto-0.4b-2"
|
| 183 |
+
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 184 |
+
model = AutoModelForSequenceClassification.from_pretrained(
|
| 185 |
+
repo, dtype=torch.bfloat16,
|
| 186 |
+
attn_implementation="{ATTENTION}",
|
| 187 |
+
).cuda().eval()
|
| 188 |
+
|
| 189 |
+
text = build_input(
|
| 190 |
+
user_request="Clean up the build artifacts and reinstall dependencies.",
|
| 191 |
+
history=[{{"tool": "Bash", "args": "ls", "result": "node_modules dist package.json"}}],
|
| 192 |
+
call={{"tool": "Bash", "args": "rm -rf node_modules dist && npm install"}},
|
| 193 |
+
)
|
| 194 |
+
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=65536).to("cuda")
|
| 195 |
+
with torch.inference_mode():
|
| 196 |
+
p_deny = model(**inputs).logits.float().softmax(-1)[0, 1].item()
|
| 197 |
+
print("deny" if p_deny >= 0.5 else "approve", p_deny)
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
Install the tested versions listed in `environment.json`, including `kernels`; the pinned Hugging Face FlashAttention kernel loads automatically on supported CUDA systems. A compatible local FlashAttention 2 installation can also use `attn_implementation="flash_attention_2"`. Ordinary SDPA is suitable for shorter inputs but can materialize large masks for long sequences.
|
| 201 |
+
|
| 202 |
+
Labels are `0 = approve`, `1 = deny`. Preserve the exact `### PROPOSED TOOL CALL`, `### USER REQUEST`, and `### AGENT HISTORY` sections. The model only consumes this serialized text; training rationales and metadata are excluded.
|
| 203 |
+
|
| 204 |
+
## Scope and limitations
|
| 205 |
+
|
| 206 |
+
This is a classification model, not a chat model. It approves routine authorized work and denies consequential unauthorized actions or actions following injected instructions. Its synthetic labels and held-out benchmark do not establish reliability on every real agent workload. A false approval remains possible; the model cannot inspect hidden file contents, resolve opaque executables, or determine a URL's runtime behavior from text alone. Long-context results are slice measurements on the supplied benchmark, not a universal guarantee. Evaluate on representative deployment traffic and treat uncertain decisions appropriately for the application.
|
| 207 |
+
|
| 208 |
+
The base model and teacher were previously developed using their own validation histories; this run prevents new training/evaluation overlap but cannot independently establish that all historical model-development decisions were untouched by public benchmarks.
|
| 209 |
+
'''
|
| 210 |
+
|
| 211 |
+
def publish(model,selected_path,result,bench,final_logits):
|
| 212 |
+
stage=CKPT/'publish';stage.mkdir(exist_ok=True,parents=True)
|
| 213 |
+
for p in pathlib.Path(selected_path).iterdir():
|
| 214 |
+
if p.is_file(): shutil.copy2(p,stage/p.name)
|
| 215 |
+
audit=json.loads((DATA/'data_audit.json').read_text());plan=json.loads((ROOT/'training_plan.json').read_text())
|
| 216 |
+
(stage/'README.md').write_text(model_card(result,audit,plan))
|
| 217 |
+
atomic_json(stage/'eval_results.json',result)
|
| 218 |
+
atomic_json(stage/'data_audit.json',audit)
|
| 219 |
+
atomic_json(stage/'training_plan.json',plan)
|
| 220 |
+
sources=json.loads((ROOT/'sources.json').read_text())
|
| 221 |
+
atomic_json(stage/'source_revisions.json',{k:{a:v[a] for a in ['repo','revision','kind']} for k,v in sources.items()})
|
| 222 |
+
shutil.copy2(ROOT/'environment.json',stage/'environment.json')
|
| 223 |
+
for name in ['download.py','prepare.py','engine.py','run.py','publish.py','sft_check.py']:
|
| 224 |
+
(stage/'training').mkdir(exist_ok=True)
|
| 225 |
+
shutil.copy2(ROOT/name,stage/'training'/name)
|
| 226 |
+
shutil.copy2(ROOT/'gate.py',stage/'gate.py')
|
| 227 |
+
np.savez(stage/'benchmark_predictions.npz',logits=final_logits,labels=np.array(bench['labels']),source_row=np.array(bench['source_row']))
|
| 228 |
+
hashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in stage.iterdir() if p.is_file()}
|
| 229 |
+
atomic_json(stage/'artifact_hashes.json',hashes)
|
| 230 |
+
api=HfApi()
|
| 231 |
+
owner=ROOT/'publish_ownership.json'
|
| 232 |
+
if not owner.exists():
|
| 233 |
+
try:
|
| 234 |
+
api.model_info(REPO)
|
| 235 |
+
except RepositoryNotFoundError:
|
| 236 |
+
api.create_repo(REPO,repo_type='model',private=True,exist_ok=False)
|
| 237 |
+
atomic_json(owner,{'repo':REPO,'created_by':'auto04-training','created_at':time.time()})
|
| 238 |
+
else: raise RuntimeError('Target repository already exists and is not owned by this run; refusing overwrite')
|
| 239 |
+
else: assert json.loads(owner.read_text())['repo']==REPO
|
| 240 |
+
commit=api.upload_folder(repo_id=REPO,repo_type='model',folder_path=str(stage),commit_message='Publish evaluated auto-0.4b-2 checkpoint and reproducibility artifacts')
|
| 241 |
+
revision=commit.oid
|
| 242 |
+
downloaded=snapshot_download(REPO,revision=revision)
|
| 243 |
+
# Verify the actual remote checkpoint after upload, before making it public.
|
| 244 |
+
reload=load_model(downloaded)
|
| 245 |
+
check=np.arange(len(bench))
|
| 246 |
+
observed=predict(reload,bench,check,'published-roundtrip')
|
| 247 |
+
reference=final_logits[check]
|
| 248 |
+
max_error=float(np.abs(observed-reference).max())
|
| 249 |
+
assert np.array_equal(observed.argmax(-1),reference.argmax(-1)), 'Remote roundtrip changed decisions'
|
| 250 |
+
assert max_error < 0.25, f'Remote roundtrip logits differ by {max_error}'
|
| 251 |
+
api.update_repo_settings(REPO,private=False)
|
| 252 |
+
remote=api.model_info(REPO,revision=revision)
|
| 253 |
+
assert not remote.private
|
| 254 |
+
record={'repo':REPO,'url':'https://huggingface.co/'+REPO,'revision':revision,'accuracy':result['benchmark_default']['overall']['accuracy'],
|
| 255 |
+
'roundtrip_examples':len(check),'roundtrip_max_abs_logit_difference':max_error,'results':str(CKPT/'final_evaluation/results.json')}
|
| 256 |
+
atomic_json(ROOT/'published.json',record)
|
| 257 |
+
event('published',**record)
|
| 258 |
+
del reload,model;gc.collect();torch.cuda.empty_cache()
|
| 259 |
+
|
| 260 |
+
if __name__=='__main__': finalize()
|
training/run.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SFT -> long-context SFT -> teacher-logit cache -> KD -> long-context KD -> audited publication."""
|
| 2 |
+
import fcntl, gc, importlib.metadata, json, pathlib, time, traceback
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from datasets import load_from_disk
|
| 6 |
+
from engine import ROOT, DATA, CKPT, LOG, SEED, atomic_json, event, baseline, load_model, predict, train_phase
|
| 7 |
+
|
| 8 |
+
PLAN = {
|
| 9 |
+
'seed': SEED,
|
| 10 |
+
'distillation_policy': 'At user request, after long SFT freeze a checkpoint on validation and evaluate Approve-or-Deny. Report the result to the user before proceeding. Distill only if its benchmark accuracy does not strictly exceed the fresh auto-1b-bf16 benchmark accuracy; skip distillation if it does. A tie triggers distillation.',
|
| 11 |
+
'sft_short': {'epochs': 2, 'lr': 1.5e-5, 'max_length_inclusion': 4096},
|
| 12 |
+
'sft_long': {'epochs': 1, 'lr': 6e-6, 'short_replay_ratio': 1.0},
|
| 13 |
+
'kd_short': {'epochs': 1, 'lr': 5e-6, 'alpha': 0.5, 'temperature': 2.0},
|
| 14 |
+
'kd_long': {'epochs': 1, 'lr': 3e-6, 'alpha': 0.5, 'temperature': 2.0, 'short_replay_ratio': 1.0},
|
| 15 |
+
'optimizer': 'Full-parameter fused AdamW; fp32 master parameters/moments; bf16 autocast; betas=(0.9,0.95); eps=1e-8; weight_decay=0.01; grad_clip=1.0',
|
| 16 |
+
'schedule': '3% linear warmup then cosine to 10% of peak learning rate in each phase',
|
| 17 |
+
'batching': 'Length buckets; <=16384 padded tokens per short microbatch; single longer example; optimizer step at >=128 examples or >=131072 true tokens',
|
| 18 |
+
'long_context': 'Every retained row is used at its full tokenized length, with model capacity of 65536; no training input required truncation.',
|
| 19 |
+
'distillation': 'Teacher forward only; temperature-scaled KL plus hard-label CE; alpha reduced to 0.125 where teacher argmax disagrees with label',
|
| 20 |
+
'selection': 'Validation-only: 0.7 * overall balanced accuracy + 0.3 * balanced accuracy at >=16384 tokens; each phase best and last compared on the full 7824-row selection split',
|
| 21 |
+
'publish_gate': {'benchmark_accuracy_min': 0.95, 'benchmark_long_accuracy_min': 0.90,
|
| 22 |
+
'false_approve_improvement_min': 0.015, 'false_deny_improvement_min': 0.025,
|
| 23 |
+
'validation_audit_accuracy_min': 0.95, 'paired_accuracy_ci95_lower_min': 0.0},
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
def cache_teacher():
|
| 27 |
+
ds=load_from_disk(str(DATA/'train'))
|
| 28 |
+
cache=CKPT/'teacher_logits.npy'; manifest=CKPT/'teacher_logits_manifest.json'
|
| 29 |
+
sources=json.loads((ROOT/'sources.json').read_text())
|
| 30 |
+
import hashlib
|
| 31 |
+
identity={'teacher_revision':sources['teacher']['revision'],'student_tokenizer_revision':sources['student']['revision'],
|
| 32 |
+
'train_order_sha256':hashlib.sha256('\n'.join(ds['text_hash']).encode()).hexdigest(),'rows':len(ds)}
|
| 33 |
+
if manifest.exists(): assert json.loads(manifest.read_text()) == identity
|
| 34 |
+
else: atomic_json(manifest, identity)
|
| 35 |
+
if cache.exists(): arr=np.load(cache,mmap_mode='r+')
|
| 36 |
+
else:
|
| 37 |
+
arr=np.lib.format.open_memmap(cache,mode='w+',dtype=np.float32,shape=(len(ds),2));arr[:]=np.nan;arr.flush()
|
| 38 |
+
assert arr.shape==(len(ds),2)
|
| 39 |
+
remaining=np.flatnonzero(~np.isfinite(arr).all(axis=1))
|
| 40 |
+
if not len(remaining): return str(cache)
|
| 41 |
+
lengths=np.array(ds['length']); remaining=remaining[np.argsort(lengths[remaining],kind='stable')]
|
| 42 |
+
model=load_model(sources['teacher']['path'])
|
| 43 |
+
blocks=[];start=0;token_count=0
|
| 44 |
+
for pos,idx in enumerate(remaining):
|
| 45 |
+
token_count+=int(lengths[idx])
|
| 46 |
+
if token_count>=5_000_000 or pos-start+1>=10000:
|
| 47 |
+
blocks.append(remaining[start:pos+1]);start=pos+1;token_count=0
|
| 48 |
+
if start<len(remaining):blocks.append(remaining[start:])
|
| 49 |
+
for block in blocks:
|
| 50 |
+
values=predict(model,ds,block,'teacher-cache',token_budget=32768)
|
| 51 |
+
arr[block]=values;arr.flush()
|
| 52 |
+
done=int(np.isfinite(arr).all(axis=1).sum())
|
| 53 |
+
event('teacher_cache',done=done,total=len(ds))
|
| 54 |
+
assert np.isfinite(arr).all()
|
| 55 |
+
del model,arr;gc.collect();torch.cuda.empty_cache()
|
| 56 |
+
return str(cache)
|
| 57 |
+
|
| 58 |
+
def run():
|
| 59 |
+
ROOT.mkdir(parents=True,exist_ok=True);CKPT.mkdir(parents=True,exist_ok=True);LOG.mkdir(parents=True,exist_ok=True)
|
| 60 |
+
atomic_json(ROOT/'training_plan.json',PLAN)
|
| 61 |
+
atomic_json(ROOT/'environment.json',{p:importlib.metadata.version(p) for p in ['torch','transformers','datasets','huggingface_hub','kernels','accelerate','numpy','scikit-learn']})
|
| 62 |
+
if (ROOT/'published.json').exists():
|
| 63 |
+
event('already_published',**json.loads((ROOT/'published.json').read_text()));return
|
| 64 |
+
if not (DATA/'data_audit.json').exists(): raise RuntimeError('Data preparation is incomplete')
|
| 65 |
+
if not json.loads((ROOT/'smoke_test.json').read_text())['passed']: raise RuntimeError('Runtime validation did not pass')
|
| 66 |
+
baseline()
|
| 67 |
+
sources=json.loads((ROOT/'sources.json').read_text())
|
| 68 |
+
ds=load_from_disk(str(DATA/'train'));lengths=np.array(ds['length'])
|
| 69 |
+
short=np.flatnonzero(lengths<=4096);long=np.flatnonzero(lengths>4096)
|
| 70 |
+
rng=np.random.default_rng(SEED)
|
| 71 |
+
replay=rng.choice(short,len(long),replace=False)
|
| 72 |
+
long_mix=np.concatenate([long,replay])
|
| 73 |
+
np.savez(CKPT/'phase_indices.npz',short=short,long=long,long_mix=long_mix)
|
| 74 |
+
p=train_phase('sft_short',sources['student']['path'],short,**{k:PLAN['sft_short'][k] for k in ['epochs','lr']})
|
| 75 |
+
p=train_phase('sft_long',p,long_mix,**{k:PLAN['sft_long'][k] for k in ['epochs','lr']})
|
| 76 |
+
from sft_check import compare_after_sft, require_result_reported
|
| 77 |
+
comparison=compare_after_sft()
|
| 78 |
+
require_result_reported()
|
| 79 |
+
p=comparison['selected_checkpoint']['path']
|
| 80 |
+
if not comparison['distillation_required']:
|
| 81 |
+
event('distillation_skipped',reason='Post-SFT benchmark accuracy exceeds the teacher',accuracy=comparison['benchmark']['overall']['accuracy'])
|
| 82 |
+
from publish import finalize
|
| 83 |
+
finalize(selected_sft=comparison['selected_checkpoint'])
|
| 84 |
+
return
|
| 85 |
+
teacher=cache_teacher()
|
| 86 |
+
p=train_phase('kd_short',p,short,teacher_logits=teacher,**{k:PLAN['kd_short'][k] for k in ['epochs','lr','alpha','temperature']})
|
| 87 |
+
p=train_phase('kd_long',p,long_mix,teacher_logits=teacher,**{k:PLAN['kd_long'][k] for k in ['epochs','lr','alpha','temperature']})
|
| 88 |
+
from publish import finalize
|
| 89 |
+
finalize()
|
| 90 |
+
|
| 91 |
+
if __name__=='__main__':
|
| 92 |
+
lock=(ROOT/'pipeline.lock').open('a')
|
| 93 |
+
fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
| 94 |
+
try:
|
| 95 |
+
run()
|
| 96 |
+
except Exception as exc:
|
| 97 |
+
event('failed',exception=type(exc).__name__,message=str(exc),traceback=traceback.format_exc())
|
| 98 |
+
raise
|
training/sft_check.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User-requested benchmark after long SFT; report it before conditional distillation."""
|
| 2 |
+
import gc, hashlib, json
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from datasets import load_from_disk
|
| 6 |
+
from engine import ROOT, DATA, CKPT, atomic_json, event, load_model, predict, report, selection_score
|
| 7 |
+
|
| 8 |
+
def needs_distillation(student_accuracy, teacher_accuracy):
|
| 9 |
+
# A tie is not better. Both measurements use the same 3,000 rows and threshold.
|
| 10 |
+
return student_accuracy <= teacher_accuracy
|
| 11 |
+
|
| 12 |
+
def compare_after_sft():
|
| 13 |
+
ready=ROOT/'post_sft_benchmark.json'
|
| 14 |
+
if ready.exists(): return json.loads(ready.read_text())
|
| 15 |
+
assert (CKPT/'sft_long/complete.json').exists(), 'Long SFT must finish first'
|
| 16 |
+
evaluated=CKPT/'post_sft_evaluation';evaluated.mkdir(parents=True,exist_ok=True)
|
| 17 |
+
val=load_from_disk(str(DATA/'validation'));bench=load_from_disk(str(DATA/'benchmark'))
|
| 18 |
+
selection=np.load(DATA/'validation_partitions.npz')['selection']
|
| 19 |
+
frozen_path=evaluated/'frozen_selection.json'
|
| 20 |
+
if frozen_path.exists(): chosen=json.loads(frozen_path.read_text())
|
| 21 |
+
else:
|
| 22 |
+
candidates=[]
|
| 23 |
+
for which in ['best','last']:
|
| 24 |
+
path=CKPT/'sft_long'/which
|
| 25 |
+
model=load_model(path)
|
| 26 |
+
logits=predict(model,val,selection,'post-sft-'+which+'-selection',evaluated/f'{which}-selection.npz')
|
| 27 |
+
r=report(val,selection,logits)
|
| 28 |
+
candidates.append({'path':str(path),'name':'sft_long-'+which,'score':selection_score(r),'distillation_optimizer_steps':0,'report':r})
|
| 29 |
+
del model;gc.collect();torch.cuda.empty_cache()
|
| 30 |
+
atomic_json(evaluated/'selection_candidates.json',candidates)
|
| 31 |
+
chosen=max(candidates,key=lambda x:(x['score'],-x['report']['overall']['nll']))
|
| 32 |
+
atomic_json(frozen_path,chosen)
|
| 33 |
+
model=load_model(chosen['path'])
|
| 34 |
+
logits=predict(model,bench,np.arange(len(bench)),'post-sft-benchmark',evaluated/'benchmark.npz')
|
| 35 |
+
r=report(bench,np.arange(len(bench)),logits)
|
| 36 |
+
teacher=json.loads((CKPT/'baseline/teacher.json').read_text())['benchmark']
|
| 37 |
+
original=json.loads((CKPT/'baseline/student.json').read_text())['benchmark']
|
| 38 |
+
required=needs_distillation(r['overall']['accuracy'],teacher['overall']['accuracy'])
|
| 39 |
+
result={'selected_checkpoint':chosen,'benchmark':r,'teacher_benchmark':teacher,'original_benchmark':original,
|
| 40 |
+
'distillation_required':required,
|
| 41 |
+
'decision_rule':'Distill only if post-long-SFT default-threshold benchmark accuracy does not strictly exceed the same-run auto-1b-bf16 benchmark accuracy. A tie triggers distillation.',
|
| 42 |
+
'benchmark_revision':json.loads((ROOT/'sources.json').read_text())['benchmark']['revision'],
|
| 43 |
+
'evaluation_note':'Checkpoint frozen using validation only before this benchmark. At user request, this benchmark determines whether to run distillation; it is therefore used for that training-procedure decision. Benchmark examples never become training or distillation targets.'}
|
| 44 |
+
atomic_json(ready,result)
|
| 45 |
+
event('post_sft_benchmark_ready',accuracy=r['overall']['accuracy'],teacher_accuracy=teacher['overall']['accuracy'],
|
| 46 |
+
distillation_required=required,report_path=str(ready))
|
| 47 |
+
del model;gc.collect();torch.cuda.empty_cache()
|
| 48 |
+
return result
|
| 49 |
+
|
| 50 |
+
def require_result_reported():
|
| 51 |
+
result_file=ROOT/'post_sft_benchmark.json';ack_file=ROOT/'sft_result_reported.json'
|
| 52 |
+
digest=hashlib.sha256(result_file.read_bytes()).hexdigest()
|
| 53 |
+
if not ack_file.exists() or json.loads(ack_file.read_text()).get('benchmark_result_sha256')!=digest:
|
| 54 |
+
event('awaiting_sft_result_report',report_path=str(result_file),benchmark_result_sha256=digest,
|
| 55 |
+
note='User requested this result before any conditional distillation. The task heartbeat must report it, record acknowledgement, and resume the service.')
|
| 56 |
+
raise SystemExit(76)
|
training_plan.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"seed": 20260908,
|
| 3 |
+
"distillation_policy": "At user request, after long SFT freeze a checkpoint on validation and evaluate Approve-or-Deny. Report the result to the user before proceeding. Distill only if its benchmark accuracy does not strictly exceed the fresh auto-1b-bf16 benchmark accuracy; skip distillation if it does. A tie triggers distillation.",
|
| 4 |
+
"sft_short": {
|
| 5 |
+
"epochs": 2,
|
| 6 |
+
"lr": 1.5e-05,
|
| 7 |
+
"max_length_inclusion": 4096
|
| 8 |
+
},
|
| 9 |
+
"sft_long": {
|
| 10 |
+
"epochs": 1,
|
| 11 |
+
"lr": 6e-06,
|
| 12 |
+
"short_replay_ratio": 1.0
|
| 13 |
+
},
|
| 14 |
+
"kd_short": {
|
| 15 |
+
"epochs": 1,
|
| 16 |
+
"lr": 5e-06,
|
| 17 |
+
"alpha": 0.5,
|
| 18 |
+
"temperature": 2.0
|
| 19 |
+
},
|
| 20 |
+
"kd_long": {
|
| 21 |
+
"epochs": 1,
|
| 22 |
+
"lr": 3e-06,
|
| 23 |
+
"alpha": 0.5,
|
| 24 |
+
"temperature": 2.0,
|
| 25 |
+
"short_replay_ratio": 1.0
|
| 26 |
+
},
|
| 27 |
+
"optimizer": "Full-parameter fused AdamW; fp32 master parameters/moments; bf16 autocast; betas=(0.9,0.95); eps=1e-8; weight_decay=0.01; grad_clip=1.0",
|
| 28 |
+
"schedule": "3% linear warmup then cosine to 10% of peak learning rate in each phase",
|
| 29 |
+
"batching": "Length buckets; <=16384 padded tokens per short microbatch; single longer example; optimizer step at >=128 examples or >=131072 true tokens",
|
| 30 |
+
"long_context": "Every retained row is used at its full tokenized length, with model capacity of 65536; no training input required truncation.",
|
| 31 |
+
"distillation": "Teacher forward only; temperature-scaled KL plus hard-label CE; alpha reduced to 0.125 where teacher argmax disagrees with label",
|
| 32 |
+
"selection": "Validation-only: 0.7 * overall balanced accuracy + 0.3 * balanced accuracy at >=16384 tokens; each phase best and last compared on the full 7824-row selection split",
|
| 33 |
+
"publish_gate": {
|
| 34 |
+
"benchmark_accuracy_min": 0.95,
|
| 35 |
+
"benchmark_long_accuracy_min": 0.9,
|
| 36 |
+
"false_approve_improvement_min": 0.015,
|
| 37 |
+
"false_deny_improvement_min": 0.025,
|
| 38 |
+
"validation_audit_accuracy_min": 0.95,
|
| 39 |
+
"paired_accuracy_ci95_lower_min": 0.0
|
| 40 |
+
}
|
| 41 |
+
}
|