aidendle94 commited on
Commit
ace79dd
·
verified ·
1 Parent(s): 7dd6dc4

Card: training data, recipe, tokens and time; how the head was edited by hand. Add the fine-tune run record.

Browse files
Files changed (2) hide show
  1. README.md +51 -2
  2. provenance/train_run.json +185 -0
README.md CHANGED
@@ -320,8 +320,57 @@ All splits below are out of distribution for the model: none of their items or i
320
  ## Provenance
321
  <hr style="margin: -0.7em 0 1.2em 0; border: 0; border-top: 1px solid rgba(127, 127, 127, 0.3);" />
322
 
323
- 1. **Fine-tune.** d1-2B, full-parameter with the tied token embeddings / LM head frozen, two epochs on a family-deduplicated mix of agent-action security data (CLI command risk labels, agent traces with needs-review and injection labels, authorization labels), up to 16 questions per sequence. Checkpoint `aidendle94/d1-guard-ft-v0-20261028` (private).
324
- 2. **Head edit.** Fine-tuning left several features intact in the last layer while the fixed head read them poorly: linear probes on last-layer activations separate LlamaFirewall injection at 98.9 AUROC (head: 87.4 on the dev split), ATBench injection at 89 (head: 74) and ATBench needs-review at 89 (head: 74). Three unit directions were fit on the final-norm output at the answer slot, each made orthogonal to what the other yes/no questions read and to the earlier directions, and added to the head rows of the "yes" tokens and subtracted from the "no" tokens: LlamaFirewall injection (weight 1.0), ATBench injection recorded under served packing (0.7), and needs-review pooled over ATBench and 900 in-distribution rows (0.5). The head was untied from the input embeddings so the input side is untouched. 10,240 weights changed. The directions and the full record, including the simulated effect on every probe, are in `provenance/`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  Modified files relative to d1-2B, as the license requires us to say: `model.safetensors` (all weights fine-tuned; `lm_head.weight` added), `config.json` (`tie_word_embeddings: false`), `calibration.json` (served temperature 1.0), `questions.json` (new). See `NOTICE`.
327
 
 
320
  ## Provenance
321
  <hr style="margin: -0.7em 0 1.2em 0; border: 0; border-top: 1px solid rgba(127, 127, 127, 0.3);" />
322
 
323
+ Scantron Preview was made in two stages: a full fine-tune of d1-2B on one rented GPU, then a hand edit of five rows of the output head, planned and carried out by an AI agent on a laptop. Both are described below; the run record and the edit record are in `provenance/`.
324
+
325
+ ### Stage 1: fine-tune
326
+
327
+ **Data.** 92,577 training rows from six public datasets, converted into one JSON state format (the user's request, the proposed tool call, recent actions, untrusted content, destination) and labeled with the 21 questions in `questions.json`. Rows were deduplicated by action template and split by family so that no command prefix or trace family appears on both sides; 10% of families were held out as the in-distribution eval. Every row that shared a key with the external benchmarks in the Performance section was excluded before training.
328
+
329
+ | Source | Train rows | Labels contributed |
330
+ |---|---|---|
331
+ | ProCreations/auto-1b-data | 49,863 | authorization, injection |
332
+ | karanxa/agent-action-safety-dataset | 21,147 | needs review, authorization, injection |
333
+ | yannelli/cli-command-risk | 10,064 | risk level, risk categories, reversible, requires root |
334
+ | tomngdev/tool-safety-v2 | 8,238 | needs review |
335
+ | westenfelder/NL2SH-ALFA | 1,678 | risk level, categories, reversible, requires root |
336
+ | b-mc2/cli-commands-explained | 1,587 | risk level, categories, reversible, requires root |
337
+
338
+ Label coverage over the training rows: authorization 70,863 (38,024 authorized / 32,839 unauthorized), needs review 35,202, injection 31,295 (1,977 positive), risk level 14,831, risk categories 14,743, reversible 14,125, requires root 13,079. Risk labels on shell commands were adjudicated by a second model where the source and a labeling model disagreed; rows that stayed ambiguous were dropped.
339
+
340
+ **Objective.** d1's native training view: each row's state is followed by its labeled questions in random order, packed up to 16 per sequence, and the loss is cross entropy over the option scores read at each question's answer slot (the best token form of each option, max-pooled). Nothing is generated. The 8,192-token state budget truncates untrusted content first, oldest first, and a row whose untrusted content was cut has its injection label masked.
341
+
342
+ **Recipe and cost.**
343
+
344
+ | | |
345
+ |---|---|
346
+ | Trainable parameters | 1.47B (the 24 backbone layers and the final norm); token embeddings / LM head (0.26B) and the vision tower frozen |
347
+ | Optimizer | AdamW, lr 1e-5, betas 0.9 / 0.999, weight decay 0.01 on matrices only, gradient clip 1.0 |
348
+ | Schedule | 3% linear warmup (98 steps), cosine to zero |
349
+ | Batch | 8 micro-batches of 8,192 padded tokens per optimizer step; 3,293 steps over 2 epochs |
350
+ | Precision | bf16 weights and compute, fp32 master weights, fp32 gradient accumulation and optimizer state |
351
+ | Tokens processed | 171.9M (85.9M per epoch), 706,060 labeled decisions |
352
+ | Hardware and time | one NVIDIA RTX 6000D (rented), 4 h 46 min wall clock, 10,014 tokens/s, 54.4 GB peak memory |
353
+ | Checkpoint | epoch 2, `aidendle94/d1-guard-ft-v0-20261028` (private) |
354
+
355
+ ### Stage 2: editing the weights by hand
356
+
357
+ The second stage used no gradient steps and no GPU rental. It was done on an Apple M3 Max laptop (128 GB, Metal) by [Claude Fable 5.1](https://www.anthropic.com/claude/fable) working as an autonomous agent in Claude Code: the agent designed the probes, ran them through the served engine, chose the edit, verified it, and wrote this card. A second model (DeepSeek V4.1 Flash, run locally) reviewed the labels and the code. No human labeled data or reviewed the edit.
358
+
359
+ **What was found.** Fine-tuning moved the backbone while the output head stayed frozen, and the two drifted apart: the last layer still carried features that the head no longer read well. Linear probes trained on the last-layer activation at the answer slot separated LlamaFirewall injection at 98.9 AUROC where the head read 87.4 on the same dev rows, ATBench injection at 89 where the head read 74, and ATBench needs-review at 89 where the head read 74. The fix is to point the head at the feature, which is a change to a handful of rows of one matrix.
360
+
361
+ **How the edit works.** Each yes/no question is answered by comparing the logits of the "yes" token forms against the "no" token forms at the answer slot. Those logits are dot products of the final-norm output `x` with the head rows of those tokens, so adding a unit direction `w` with weight `β` to the "yes" rows and subtracting it from the "no" rows shifts the served yes-vs-no log-odds by exactly `2β (w · x)`. Because the shift is linear in `x`, its effect on every recorded probe can be computed offline before anything is served, and a candidate edit can be rejected in seconds.
362
+
363
+ **Procedure.**
364
+
365
+ 1. **Record.** For each question of interest, run the fine-tuned model over a dev split through the served engine (same prompt, same packing, same 16-question sets) and keep the hidden state of every layer at the question's answer slot. Probes were recorded under the exact question set the model is served with, because a d1 answer depends on which other questions share its set.
366
+ 2. **Locate.** Fit a logistic-regression probe at every layer and compare its AUROC with the head's. The gap at the last layer is the readout loss; it was large for injection and needs-review and near zero for the shell-command questions, which were left alone.
367
+ 3. **Fit the direction.** Logistic regression on the final-norm output at the answer slot (standardized features, C = 0.01), normalized to a unit vector. Three directions: LlamaFirewall injection (654 rows, 50 positive), ATBench injection recorded under served packing (80 rows, 18 positive), and needs-review pooled over ATBench and 900 in-distribution rows (1,077 rows, 652 positive).
368
+ 4. **Protect the other questions.** Because the "yes" and "no" rows are shared by all 20 yes/no questions, each direction was made orthogonal (QR) to the mean activation and top four principal components of every other question's probe, and to the directions fit before it. Cosine between the raw and the protected direction: 0.91, 0.93 and 0.94, so most of the signal survived.
369
+ 5. **Simulate.** Compute the shifted log-odds `s + 2 (δ · x)` with `δ = 1.0 w₁ + 0.7 w₂ + 0.5 w₃` on all 20 recorded probes: AUROC before and after, mean shift, and how many rows cross the decision point. Weights were chosen where the targets improved and the guard questions moved by at most one or two examples each.
370
+ 6. **Write the weights.** Untie the head from the input embeddings (so the input side is untouched), add `δ` (norm 1.32) to the two "yes" token rows and subtract it from the three "no" token rows. 5 rows × 2,048 = 10,240 weights changed out of 1.9B; everything else is byte-identical to the fine-tuned checkpoint.
371
+ 7. **Verify by serving.** Re-run the edited model on the dev splits and confirm the offline simulation (correlation 1.0 with the served shift), then score the frozen test splits once. The frozen results are the "Scantron Preview" column in the Performance section; the pre-edit column is the same checkpoint without the edit.
372
+
373
+ The in-sample fit AUROC of each direction before and after the edit was 0.875 → 0.993 (LlamaFirewall injection), 0.741 → 0.928 (ATBench injection) and 0.978 → 0.981 (needs-review). `provenance/weight_edit.json` has the full record, including the simulated effect on every probe, and `provenance/head_delta.npy` is `δ` itself, so the edit can be undone or re-weighted.
374
 
375
  Modified files relative to d1-2B, as the license requires us to say: `model.safetensors` (all weights fine-tuned; `lm_head.weight` added), `config.json` (`tie_word_embeddings: false`), `calibration.json` (served temperature 1.0), `questions.json` (new). See `NOTICE`.
376
 
provenance/train_run.json ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run_id": "ft-v0-20261028",
3
+ "status": "done",
4
+ "base_checkpoint": {
5
+ "repo": "LiquidAI/d1-2B",
6
+ "revision": "cfe5760ff5d5998f91d9ac56bd4b2dbd96996d41",
7
+ "local_path": "/workspace/d1-guard/models/d1-2B"
8
+ },
9
+ "dataset_version": {
10
+ "dataset_version": "ds-20261028-05072130",
11
+ "files": {
12
+ "clean/mix_v2/train.parquet": {
13
+ "sha256": "7f405a8c8b897461e502a6bbd488bd5a54d1b50761b38e43842444ce7d58d601"
14
+ },
15
+ "clean/mix_v2/eval.parquet": {
16
+ "sha256": "ff06a2dc9faac0eb51b1d4ca9615b631098905e832925f4f12b60d6bd4bbe76f"
17
+ }
18
+ },
19
+ "mix_manifest": "clean/mix_v2/manifest.json",
20
+ "note": "mix_v2 is not listed in DATASET_VERSION.json (it lists mix_v1); the mix files are pinned by the sha256 above"
21
+ },
22
+ "split_and_family_definitions": "scripts/40_build_mix.py (mix_v2); cli_capped / auth_aod_bench per DATASET_VERSION.json",
23
+ "taxonomy_version": "v0-draft-2026-10-27",
24
+ "question_set_version": "questions-v0-2026-10-28 (training/questions_v0.json)",
25
+ "trainable_parameter_set": {
26
+ "description": "language backbone: model.language_model.layers.* and model.language_model.embedding_norm",
27
+ "n_trainable": 1466657536,
28
+ "n_frozen": 451524352,
29
+ "tensors_trainable": 285,
30
+ "frozen": [
31
+ "vision_tower",
32
+ "multi_modal_projector",
33
+ "embeddings (embed_tokens, tied to lm_head)"
34
+ ]
35
+ },
36
+ "method": "full",
37
+ "objective": "d1 tree: cross entropy over each labeled question's option scores (max-pooled token forms at the answer slot), questions packed after their state in random order, groups of set_size",
38
+ "seed": 0,
39
+ "config": {
40
+ "lr": 1e-05,
41
+ "optimizer": "adamw (fused, betas 0.9/0.999, eps 1e-8)",
42
+ "weight_decay": 0.01,
43
+ "weight_decay_on": "matrices only (not norms)",
44
+ "schedule": "linear warmup then cosine to 0",
45
+ "warmup": 0.03,
46
+ "warmup_steps": 98,
47
+ "epochs": 2,
48
+ "precision": "bf16 weights and compute, fp32 master weights, grads accumulated in fp32, fp32 AdamW states",
49
+ "grad_clip": 1.0,
50
+ "set_size": 16,
51
+ "state_budget_tokens": 8192,
52
+ "micro_batch_padded_tokens": 8192,
53
+ "accum": 8,
54
+ "total_steps": 3293,
55
+ "grad_ckpt": false,
56
+ "loss_normalization": "mean over the step's decisions",
57
+ "truncation": "untrusted_context oldest-first, then recent_actions oldest-first; untrusted_origin masked on rows whose untrusted_context was cut"
58
+ },
59
+ "hardware": {
60
+ "worker": "vast.ai 58064827",
61
+ "gpu": "NVIDIA RTX 6000D"
62
+ },
63
+ "tokens_processed": 171862216,
64
+ "decisions_processed": 706060,
65
+ "wall_clock_s": 17176,
66
+ "throughput_tokens_per_s": 10014,
67
+ "peak_memory_gb": 54.43,
68
+ "correctness_checks": {
69
+ "see": "experiments/full_ft_v0/checks/checks.json"
70
+ },
71
+ "metrics": {
72
+ "A_public": null,
73
+ "B_private_ood": null,
74
+ "C_redteam": null,
75
+ "D_dev": null,
76
+ "paraphrase_gap": null,
77
+ "latency_ms_p50_p99": null
78
+ },
79
+ "epochs": [
80
+ {
81
+ "epoch": 1,
82
+ "step": 1647,
83
+ "tokens": 85931108,
84
+ "minutes": 146.2,
85
+ "quick_eval": {
86
+ "risk_level": {
87
+ "loss": 0.3628,
88
+ "acc": 0.8986,
89
+ "n": 552,
90
+ "hc_recall_at_0.5": 0.7941,
91
+ "safe_fpr_at_0.5": 0.0097
92
+ },
93
+ "category": {
94
+ "loss": 0.0252,
95
+ "acc": 0.993,
96
+ "n": 8265
97
+ },
98
+ "reversible": {
99
+ "loss": 0.1781,
100
+ "acc": 0.943,
101
+ "n": 544
102
+ },
103
+ "requires_root": {
104
+ "loss": 0.1824,
105
+ "acc": 0.9561,
106
+ "n": 524
107
+ },
108
+ "needs_review": {
109
+ "loss": 0.0835,
110
+ "acc": 0.9745,
111
+ "n": 432
112
+ },
113
+ "authorization": {
114
+ "loss": 0.0373,
115
+ "acc": 0.9908,
116
+ "n": 326
117
+ },
118
+ "untrusted_origin": {
119
+ "loss": 0.0213,
120
+ "acc": 0.9918,
121
+ "n": 122
122
+ },
123
+ "all": {
124
+ "loss": 0.0605,
125
+ "n": 10765
126
+ }
127
+ },
128
+ "checkpoint": "/workspace/d1-guard/checkpoints/ft-v0-20261028/epoch1"
129
+ },
130
+ {
131
+ "epoch": 2,
132
+ "step": 3293,
133
+ "tokens": 171862216,
134
+ "minutes": 285.5,
135
+ "quick_eval": {
136
+ "risk_level": {
137
+ "loss": 0.3868,
138
+ "acc": 0.9022,
139
+ "n": 552,
140
+ "hc_recall_at_0.5": 0.8627,
141
+ "safe_fpr_at_0.5": 0.0097
142
+ },
143
+ "category": {
144
+ "loss": 0.0248,
145
+ "acc": 0.9936,
146
+ "n": 8265
147
+ },
148
+ "reversible": {
149
+ "loss": 0.2041,
150
+ "acc": 0.9485,
151
+ "n": 544
152
+ },
153
+ "requires_root": {
154
+ "loss": 0.1836,
155
+ "acc": 0.9695,
156
+ "n": 524
157
+ },
158
+ "needs_review": {
159
+ "loss": 0.0612,
160
+ "acc": 0.9745,
161
+ "n": 432
162
+ },
163
+ "authorization": {
164
+ "loss": 0.0133,
165
+ "acc": 0.9939,
166
+ "n": 326
167
+ },
168
+ "untrusted_origin": {
169
+ "loss": 0.0081,
170
+ "acc": 0.9918,
171
+ "n": 122
172
+ },
173
+ "all": {
174
+ "loss": 0.061,
175
+ "n": 10765
176
+ }
177
+ },
178
+ "checkpoint": "/workspace/d1-guard/checkpoints/ft-v0-20261028/epoch2"
179
+ }
180
+ ],
181
+ "checkpoint_id": "/workspace/d1-guard/checkpoints/ft-v0-20261028/epoch2",
182
+ "log": "/workspace/d1-guard/logs/train_ft-v0-20261028.log",
183
+ "train_s": 17163,
184
+ "steps_done": 3293
185
+ }