ProCreations commited on
Commit
d158bbb
·
verified ·
1 Parent(s): 8e59e44

Publish evaluated auto-0.4b-2 checkpoint and reproducibility artifacts

Browse files
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
+ }