harness-everything 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +35 -0
- package/PRIVACY.md +9 -0
- package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
- package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
- package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
- package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
- package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
- package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
- package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
- package/ci/mechanism-34-observations.test.js +550 -0
- package/ci/mechanism-system-one-collect.test.js +16 -0
- package/ci/mechanism-system-one-intent.test.js +406 -0
- package/ci/mechanism-system-one-laya.test.js +45 -0
- package/ci/mechanism-system-one-relevance.test.js +112 -0
- package/ci/mechanism-system-one-resident.test.js +16 -0
- package/ci/system-one-laya-calibrate-test.py +65 -0
- package/ci/system-one-laya-export-test.py +155 -0
- package/ci/system-one-laya-label-test.py +201 -0
- package/ci/system-one-laya-perintent-test.py +60 -0
- package/ci/system-one-laya-provider-test.py +115 -0
- package/ci/system-one-laya-train-test.py +93 -0
- package/docs/routing.md +25 -0
- package/docs/system-one-intent-gates.md +102 -0
- package/docs/system-one-intent.md +173 -0
- package/docs/system-one-laya-training.md +38 -0
- package/docs/system-one-observations.md +206 -0
- package/docs/system-one-routing.md +25 -10
- package/docs/system-one-suggestion-gates.md +292 -0
- package/docs/system-one-training.md +56 -6
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/kernel-router.js +10 -0
- package/harness-everything/scripts/system-one/catalogs.js +35 -0
- package/harness-everything/scripts/system-one/contract.js +45 -1
- package/harness-everything/scripts/system-one/evaluate.js +115 -19
- package/harness-everything/scripts/system-one/intent.js +22 -0
- package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
- package/harness-everything/scripts/system-one/provider.js +10 -3
- package/harness-everything/scripts/system-one/resident.js +8 -3
- package/harness-everything/scripts/system-one/router.js +1 -1
- package/hooks/hooks.json +40 -0
- package/hooks/scripts/codex-user-prompt.js +39 -0
- package/hooks/scripts/lib/observations.js +581 -0
- package/hooks/scripts/observation-hook.js +25 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +35 -2
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
- package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
- package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +115 -19
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +10 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +8 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/evaluate-system-one.js +17 -5
- package/scripts/system-one-calibrate-laya.py +165 -0
- package/scripts/system-one-calibrate.js +42 -13
- package/scripts/system-one-collect.js +7 -3
- package/scripts/system-one-compare-laya.py +156 -0
- package/scripts/system-one-corpus.js +22 -15
- package/scripts/system-one-eval-laya.js +112 -0
- package/scripts/system-one-export-laya.py +197 -0
- package/scripts/system-one-label-laya.py +243 -0
- package/scripts/system-one-label.js +163 -37
- package/scripts/system-one-observations-export.js +106 -0
- package/scripts/system-one-observations-review.js +86 -0
- package/scripts/system-one-perintent-laya.py +134 -0
- package/scripts/system-one-train-laya.py +344 -0
- package/scripts/system-one-train-ngram.py +66 -14
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Single-device RLCD fine-tuning for Laya (#255 Phase 3).
|
|
3
|
+
|
|
4
|
+
Faithful single-process port of the upstream Kaggle DDP recipe
|
|
5
|
+
(notebooks/laya_finetune_typed_decisions_2xT4_kaggle.ipynb, train_ddp.py):
|
|
6
|
+
same GRPO + soft cross-entropy objective, same calibration holdout seed,
|
|
7
|
+
same temperature fit, same artifact layout. Differences: no NCCL/DDP;
|
|
8
|
+
cuda uses fp16+scaler, mps/cpu train fp32 without a scaler.
|
|
9
|
+
|
|
10
|
+
Input is the reviewable items jsonl from system-one-export-laya.py.
|
|
11
|
+
Output is a loadable checkpoint dir (model.safetensors + encoder/ +
|
|
12
|
+
tokenizer/ + rl_agent_config.json). Heavy deps import lazily so contract
|
|
13
|
+
tests run on stdlib.
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import random
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import torch
|
|
24
|
+
_TORCH = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
torch = None
|
|
27
|
+
_TORCH = False
|
|
28
|
+
|
|
29
|
+
CALIB_SEED = 20260922
|
|
30
|
+
CALIB_MAX = 400
|
|
31
|
+
INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
|
|
32
|
+
'review', 'test', 'docs', 'plan', 'investigate']
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def need_torch():
|
|
36
|
+
if not _TORCH:
|
|
37
|
+
raise RuntimeError('torch is required for training')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def intent_weights(intents, table):
|
|
41
|
+
"""Per-item loss multipliers by training intent; unlisted intents weigh 1."""
|
|
42
|
+
import math # noqa: PLC0415
|
|
43
|
+
table = dict(table or {})
|
|
44
|
+
for intent, weight in table.items():
|
|
45
|
+
if intent not in INTENTS or not isinstance(weight, (int, float)) \
|
|
46
|
+
or not math.isfinite(weight) or weight <= 0:
|
|
47
|
+
raise ValueError(f'intent-weights: bad entry {intent!r}:{weight!r}')
|
|
48
|
+
return [float(table.get(intent, 1.0)) for intent in intents]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def calib_split(items, seed=CALIB_SEED, frac=0.1):
|
|
52
|
+
"""Deterministic calibration split, grouped by prompt when promptId exists.
|
|
53
|
+
|
|
54
|
+
Calibration keeps one copy per promptId+intent identity. Oversampled copies
|
|
55
|
+
survive only in training groups, preventing exact-duplicate leakage and
|
|
56
|
+
calibration reweighting. Callers without promptId retain per-item behavior.
|
|
57
|
+
"""
|
|
58
|
+
groups = {}
|
|
59
|
+
group_order = []
|
|
60
|
+
for index, item in enumerate(items):
|
|
61
|
+
prompt_id = item.get('promptId')
|
|
62
|
+
key = ('prompt', str(prompt_id)) if prompt_id is not None else ('item', index)
|
|
63
|
+
if key not in groups:
|
|
64
|
+
groups[key] = []
|
|
65
|
+
group_order.append(key)
|
|
66
|
+
groups[key].append(item)
|
|
67
|
+
|
|
68
|
+
def calib_rows(group):
|
|
69
|
+
if not group or group[0].get('promptId') is None:
|
|
70
|
+
return list(group)
|
|
71
|
+
seen, rows = set(), []
|
|
72
|
+
for item in group:
|
|
73
|
+
identity = (item.get('promptId'), item.get('intent'))
|
|
74
|
+
if identity in seen:
|
|
75
|
+
continue
|
|
76
|
+
seen.add(identity)
|
|
77
|
+
rows.append(item)
|
|
78
|
+
return rows
|
|
79
|
+
|
|
80
|
+
base_groups = {key: calib_rows(group) for key, group in groups.items()}
|
|
81
|
+
base_total = sum(len(group) for group in base_groups.values())
|
|
82
|
+
if base_total == 0:
|
|
83
|
+
return [], []
|
|
84
|
+
n_calib = max(1, min(CALIB_MAX, base_total // 10 if frac == 0.1 else int(base_total * frac)))
|
|
85
|
+
|
|
86
|
+
shuffled = list(group_order)
|
|
87
|
+
random.Random(seed).shuffle(shuffled)
|
|
88
|
+
calib_groups, calib_count = set(), 0
|
|
89
|
+
for key in shuffled:
|
|
90
|
+
size = len(base_groups[key])
|
|
91
|
+
if calib_count == 0 and size > n_calib:
|
|
92
|
+
calib_groups.add(key)
|
|
93
|
+
break
|
|
94
|
+
if calib_count + size <= n_calib:
|
|
95
|
+
calib_groups.add(key)
|
|
96
|
+
calib_count += size
|
|
97
|
+
if calib_count == n_calib:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
train, calib = [], []
|
|
101
|
+
for key in group_order:
|
|
102
|
+
if key in calib_groups:
|
|
103
|
+
calib.extend(base_groups[key])
|
|
104
|
+
else:
|
|
105
|
+
train.extend(groups[key])
|
|
106
|
+
return train, calib
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def collate(items, pad_id):
|
|
110
|
+
"""Batch upstream-format items into padded tensors."""
|
|
111
|
+
need_torch()
|
|
112
|
+
n, max_len = len(items), max(len(it['ids']) for it in items)
|
|
113
|
+
kmax = max(len(it['markers']) for it in items)
|
|
114
|
+
ids = torch.full((n, max_len), pad_id, dtype=torch.long)
|
|
115
|
+
att = torch.zeros((n, max_len), dtype=torch.long)
|
|
116
|
+
mpos = torch.zeros((n, kmax), dtype=torch.long)
|
|
117
|
+
mmask = torch.zeros((n, kmax), dtype=torch.bool)
|
|
118
|
+
target = torch.zeros((n, kmax), dtype=torch.float32)
|
|
119
|
+
for i, it in enumerate(items):
|
|
120
|
+
ids[i, :len(it['ids'])] = torch.tensor(it['ids'])
|
|
121
|
+
att[i, :len(it['ids'])] = 1
|
|
122
|
+
k = len(it['markers'])
|
|
123
|
+
mpos[i, :k] = torch.tensor(it['markers'])
|
|
124
|
+
mmask[i, :k] = True
|
|
125
|
+
target[i, :len(it['target'])] = torch.tensor(it['target'], dtype=torch.float32)
|
|
126
|
+
return {'input_ids': ids, 'attention_mask': att, 'marker_pos': mpos, 'marker_mask': mmask,
|
|
127
|
+
'target': target,
|
|
128
|
+
'qtype': torch.tensor([it['qtype'] for it in items]),
|
|
129
|
+
'label': torch.tensor([it['label'] for it in items])}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def fit_one_temp(sel):
|
|
133
|
+
"""Fit one temperature per question type with LBFGS (upstream recipe)."""
|
|
134
|
+
need_torch()
|
|
135
|
+
if len(sel) < 10:
|
|
136
|
+
return 1.0
|
|
137
|
+
kmax = max(len(z) for z, _ in sel)
|
|
138
|
+
Z = torch.full((len(sel), kmax), -1e4)
|
|
139
|
+
T = torch.zeros((len(sel), kmax))
|
|
140
|
+
for i, (z, t) in enumerate(sel):
|
|
141
|
+
Z[i, :len(z)] = torch.tensor(z)
|
|
142
|
+
T[i, :len(t)] = torch.tensor(t, dtype=torch.float32)
|
|
143
|
+
log_t = torch.zeros(1, requires_grad=True)
|
|
144
|
+
opt = torch.optim.LBFGS([log_t], lr=0.1, max_iter=100)
|
|
145
|
+
|
|
146
|
+
def closure():
|
|
147
|
+
opt.zero_grad()
|
|
148
|
+
loss = -(T * torch.log_softmax(Z / log_t.exp(), -1)).sum(-1).mean()
|
|
149
|
+
loss.backward()
|
|
150
|
+
return loss
|
|
151
|
+
|
|
152
|
+
opt.step(closure)
|
|
153
|
+
return float(torch.clamp(log_t.exp(), 0.1, 10.0).item())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def pick_device(name):
|
|
157
|
+
need_torch()
|
|
158
|
+
if name == 'auto':
|
|
159
|
+
if torch.cuda.is_available():
|
|
160
|
+
return 'cuda'
|
|
161
|
+
if hasattr(torch, 'backends') and torch.backends.mps.is_available():
|
|
162
|
+
return 'mps'
|
|
163
|
+
return 'cpu'
|
|
164
|
+
return name
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def train(args):
|
|
168
|
+
need_torch()
|
|
169
|
+
from safetensors.torch import load_file, save_file # noqa: PLC0415
|
|
170
|
+
from transformers import AutoTokenizer # noqa: PLC0415
|
|
171
|
+
from laya.agent import _fix_tokenizer_config # noqa: PLC0415
|
|
172
|
+
from laya.common import build_model, proper_reward # noqa: PLC0415
|
|
173
|
+
|
|
174
|
+
device = pick_device(args.device)
|
|
175
|
+
use_amp = device == 'cuda'
|
|
176
|
+
out_dir = Path(args.out)
|
|
177
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
|
|
179
|
+
items = [json.loads(line) for line in Path(args.items).read_text(encoding='utf-8').splitlines() if line.strip()]
|
|
180
|
+
train_items, calib_items = calib_split(items, seed=args.calib_seed, frac=args.calib_frac)
|
|
181
|
+
weights_table = json.loads(args.intent_weights)
|
|
182
|
+
intent_weights([], weights_table) # validate early, before GPU time
|
|
183
|
+
print(json.dumps({'trainItems': len(train_items), 'calibItems': len(calib_items), 'device': device,
|
|
184
|
+
'intentWeights': weights_table}), flush=True)
|
|
185
|
+
|
|
186
|
+
from huggingface_hub import snapshot_download # noqa: PLC0415
|
|
187
|
+
model_dir = Path(snapshot_download(args.checkpoint))
|
|
188
|
+
base = model_dir / args.subfolder if args.subfolder else model_dir
|
|
189
|
+
_fix_tokenizer_config(str(base))
|
|
190
|
+
with open(base / 'rl_agent_config.json', encoding='utf-8') as fh:
|
|
191
|
+
cfg = json.load(fh)
|
|
192
|
+
cfg['max_tokens_per_batch'] = 4096
|
|
193
|
+
cfg['max_len'] = 1024
|
|
194
|
+
cfg['head_max_len'] = 256
|
|
195
|
+
tok = AutoTokenizer.from_pretrained(str(base / 'tokenizer'))
|
|
196
|
+
model = build_model(cfg, encoder_dir=str(base / 'encoder'))
|
|
197
|
+
model.load_state_dict(load_file(str(base / 'model.safetensors')), strict=True)
|
|
198
|
+
model.encoder.gradient_checkpointing_enable(gradient_checkpointing_kwargs={'use_reentrant': False})
|
|
199
|
+
model.head_checkpointing = True
|
|
200
|
+
model.to(device)
|
|
201
|
+
model.train()
|
|
202
|
+
|
|
203
|
+
enc_params = [p for n, p in model.named_parameters() if 'encoder.' in n]
|
|
204
|
+
head_params = [p for n, p in model.named_parameters() if 'encoder.' not in n]
|
|
205
|
+
optimizer = torch.optim.AdamW([
|
|
206
|
+
{'params': enc_params, 'lr': args.lr_encoder},
|
|
207
|
+
{'params': head_params, 'lr': args.lr_head},
|
|
208
|
+
], weight_decay=0.01)
|
|
209
|
+
updates_per_epoch = max(1, len(train_items) // (args.micro_batch * args.grad_accum))
|
|
210
|
+
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
|
|
211
|
+
optimizer, T_max=max(1, updates_per_epoch * args.epochs), eta_min=1e-6)
|
|
212
|
+
scaler = torch.amp.GradScaler('cuda', enabled=use_amp)
|
|
213
|
+
|
|
214
|
+
t0 = time.time()
|
|
215
|
+
for epoch in range(args.epochs):
|
|
216
|
+
rnd = random.Random(42 + epoch)
|
|
217
|
+
rnd.shuffle(train_items)
|
|
218
|
+
sigma = args.sigma_start + (args.sigma_end - args.sigma_start) * (epoch / max(1, args.epochs - 1))
|
|
219
|
+
epoch_loss, n_batches = 0.0, 0
|
|
220
|
+
optimizer.zero_grad(set_to_none=True)
|
|
221
|
+
accum = 0
|
|
222
|
+
for b in range(0, len(train_items), args.micro_batch):
|
|
223
|
+
chunk = train_items[b:b + args.micro_batch]
|
|
224
|
+
batch = collate(chunk, tok.pad_token_id)
|
|
225
|
+
ctx = torch.amp.autocast('cuda', dtype=torch.float16) if use_amp else torch.amp.autocast('cpu', enabled=False)
|
|
226
|
+
with ctx:
|
|
227
|
+
logits, act = model(batch['input_ids'].to(device), batch['attention_mask'].to(device),
|
|
228
|
+
batch['marker_pos'].to(device), batch['marker_mask'].to(device),
|
|
229
|
+
batch['qtype'].to(device))
|
|
230
|
+
logits = logits.float()
|
|
231
|
+
mask = batch['marker_mask'].to(device)
|
|
232
|
+
k = mask.sum(-1, keepdim=True).float()
|
|
233
|
+
target = batch['target'].to(device)
|
|
234
|
+
eps = torch.randn((args.group_size,) + logits.shape, device=device) * sigma * mask
|
|
235
|
+
eps = (eps - eps.sum(-1, keepdim=True) / k) * mask
|
|
236
|
+
z = logits.detach().unsqueeze(0) + eps
|
|
237
|
+
q = torch.softmax(z.masked_fill(~mask, -1e4), -1)
|
|
238
|
+
with torch.no_grad():
|
|
239
|
+
r = proper_reward(q, target.unsqueeze(0), batch['qtype'].to(device), mask,
|
|
240
|
+
w_sph=0.75, w_rps=1.0)
|
|
241
|
+
adv = r - r.mean(0, keepdim=True)
|
|
242
|
+
adv = adv / (adv.std() + 1e-6)
|
|
243
|
+
logp = -(((z - logits.unsqueeze(0)) ** 2) * mask).sum(-1) / (2 * sigma ** 2)
|
|
244
|
+
sample_w = torch.tensor(
|
|
245
|
+
intent_weights([it.get('intent') for it in chunk], weights_table),
|
|
246
|
+
dtype=torch.float32, device=device)
|
|
247
|
+
loss_rl = (-(adv * logp).mean(0) * sample_w).mean()
|
|
248
|
+
loss_ce = ((-(target * torch.log_softmax(logits.masked_fill(~mask, -1e4), -1)).sum(-1)) * sample_w).mean()
|
|
249
|
+
loss = (loss_rl + loss_ce) / args.grad_accum + 0.0 * act.sum()
|
|
250
|
+
scaler.scale(loss).backward()
|
|
251
|
+
accum += 1
|
|
252
|
+
if accum % args.grad_accum == 0 or (b + args.micro_batch) >= len(train_items):
|
|
253
|
+
scaler.unscale_(optimizer)
|
|
254
|
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
255
|
+
scaler.step(optimizer)
|
|
256
|
+
scaler.update()
|
|
257
|
+
scheduler.step()
|
|
258
|
+
optimizer.zero_grad(set_to_none=True)
|
|
259
|
+
epoch_loss += loss.item() * args.grad_accum
|
|
260
|
+
n_batches += 1
|
|
261
|
+
if n_batches % 50 == 0:
|
|
262
|
+
print(json.dumps({'epoch': epoch + 1, 'step': n_batches,
|
|
263
|
+
'loss': round(loss.item() * args.grad_accum, 4),
|
|
264
|
+
'reward': round(r.mean().item(), 3)}), flush=True)
|
|
265
|
+
ckpt = out_dir / 'checkpoint_latest'
|
|
266
|
+
ckpt.mkdir(exist_ok=True)
|
|
267
|
+
ckpt_sd = {k: v.half().contiguous().cpu() for k, v in model.state_dict().items()}
|
|
268
|
+
save_file(ckpt_sd, str(ckpt / 'model.safetensors'))
|
|
269
|
+
model.encoder.config.save_pretrained(str(ckpt / 'encoder'))
|
|
270
|
+
tok.save_pretrained(str(ckpt / 'tokenizer'))
|
|
271
|
+
(ckpt / 'checkpoint_meta.json').write_text(json.dumps(
|
|
272
|
+
{'epoch': epoch + 1, 'totalEpochs': args.epochs,
|
|
273
|
+
'avgLoss': epoch_loss / max(1, n_batches)}), encoding='utf-8')
|
|
274
|
+
print(json.dumps({'epochDone': epoch + 1, 'avgLoss': round(epoch_loss / max(1, n_batches), 4),
|
|
275
|
+
'elapsedS': round(time.time() - t0, 1)}), flush=True)
|
|
276
|
+
if device in ('cuda', 'mps'):
|
|
277
|
+
getattr(torch, device).empty_cache()
|
|
278
|
+
|
|
279
|
+
print(json.dumps({'fittingTemperatures': True}), flush=True)
|
|
280
|
+
del optimizer, scaler, scheduler
|
|
281
|
+
if device == 'cuda':
|
|
282
|
+
torch.cuda.empty_cache()
|
|
283
|
+
model.eval()
|
|
284
|
+
calib_preds = []
|
|
285
|
+
with torch.no_grad():
|
|
286
|
+
for c in range(0, len(calib_items), 16):
|
|
287
|
+
chunk = calib_items[c:c + 16]
|
|
288
|
+
cb = collate(chunk, tok.pad_token_id)
|
|
289
|
+
ctx = torch.amp.autocast('cuda', dtype=torch.float16) if use_amp else torch.amp.autocast('cpu', enabled=False)
|
|
290
|
+
with ctx:
|
|
291
|
+
l_sub, _ = model(cb['input_ids'].to(device), cb['attention_mask'].to(device),
|
|
292
|
+
cb['marker_pos'].to(device), cb['marker_mask'].to(device),
|
|
293
|
+
cb['qtype'].to(device))
|
|
294
|
+
l_np = l_sub.float().cpu().numpy()
|
|
295
|
+
for r, it in enumerate(chunk):
|
|
296
|
+
k = len(it['markers'])
|
|
297
|
+
calib_preds.append((it['qtype'], l_np[r, :k], it['target']))
|
|
298
|
+
fitted = [1.2, 1.2, 1.2]
|
|
299
|
+
try:
|
|
300
|
+
for qt in range(3):
|
|
301
|
+
sel = [(z, t) for q_type, z, t in calib_preds if q_type == qt]
|
|
302
|
+
if sel:
|
|
303
|
+
fitted[qt] = fit_one_temp(sel)
|
|
304
|
+
except Exception as exc: # noqa: BLE001
|
|
305
|
+
print(json.dumps({'temperatureFallback': str(exc)}), flush=True)
|
|
306
|
+
sd = {k: v.half().contiguous().cpu() for k, v in model.state_dict().items()}
|
|
307
|
+
save_file(sd, str(out_dir / 'model.safetensors'))
|
|
308
|
+
model.encoder.config.save_pretrained(str(out_dir / 'encoder'))
|
|
309
|
+
tok.save_pretrained(str(out_dir / 'tokenizer'))
|
|
310
|
+
cfg['fine_tuned'] = True
|
|
311
|
+
cfg['model_name'] = args.model_name
|
|
312
|
+
cfg['temperature'] = fitted
|
|
313
|
+
cfg.pop('temperature_by_options', None)
|
|
314
|
+
(out_dir / 'rl_agent_config.json').write_text(json.dumps(cfg, indent=2), encoding='utf-8')
|
|
315
|
+
print(json.dumps({'done': True, 'out': str(out_dir), 'temperatures': [round(t, 3) for t in fitted],
|
|
316
|
+
'elapsedS': round(time.time() - t0, 1)}), flush=True)
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def main(argv=None):
|
|
321
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
322
|
+
parser.add_argument('--items', required=True, help='items jsonl from system-one-export-laya.py')
|
|
323
|
+
parser.add_argument('--out', required=True, help='checkpoint output dir')
|
|
324
|
+
parser.add_argument('--checkpoint', default='convaiinnovations/laya')
|
|
325
|
+
parser.add_argument('--subfolder', default='multilingual')
|
|
326
|
+
parser.add_argument('--model-name', default='laya-harness-intent-s1')
|
|
327
|
+
parser.add_argument('--device', default='auto', choices=['auto', 'cuda', 'mps', 'cpu'])
|
|
328
|
+
parser.add_argument('--epochs', type=int, default=2)
|
|
329
|
+
parser.add_argument('--micro-batch', type=int, default=2)
|
|
330
|
+
parser.add_argument('--grad-accum', type=int, default=8)
|
|
331
|
+
parser.add_argument('--group-size', type=int, default=4)
|
|
332
|
+
parser.add_argument('--lr-encoder', type=float, default=2.5e-5)
|
|
333
|
+
parser.add_argument('--lr-head', type=float, default=1e-4)
|
|
334
|
+
parser.add_argument('--sigma-start', type=float, default=0.4)
|
|
335
|
+
parser.add_argument('--sigma-end', type=float, default=0.1)
|
|
336
|
+
parser.add_argument('--calib-seed', type=int, default=CALIB_SEED)
|
|
337
|
+
parser.add_argument('--calib-frac', type=float, default=0.1)
|
|
338
|
+
parser.add_argument('--intent-weights', default='{}', help='JSON {intent: multiplier} for loss weighting')
|
|
339
|
+
args = parser.parse_args(argv)
|
|
340
|
+
return train(args)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == '__main__':
|
|
344
|
+
sys.exit(main())
|
|
@@ -15,6 +15,7 @@ import sys
|
|
|
15
15
|
sys.dont_write_bytecode = True
|
|
16
16
|
REPO = Path(__file__).resolve().parents[1]
|
|
17
17
|
DEFAULT_HOLDOUT = REPO / 'benchmarks' / 'fixtures' / 'system-one-holdout.json'
|
|
18
|
+
INTENT_HOLDOUT = REPO / 'benchmarks' / 'fixtures' / 'system-one-intent-holdout.json'
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def fnv1a32(text):
|
|
@@ -56,12 +57,27 @@ def train(args):
|
|
|
56
57
|
|
|
57
58
|
torch.manual_seed(args.seed)
|
|
58
59
|
data = Path(args.data_dir)
|
|
59
|
-
|
|
60
|
+
intent = args.task == 'intent'
|
|
61
|
+
scored = args.target == 'scores'
|
|
62
|
+
holdout = args.holdout or (INTENT_HOLDOUT if intent else DEFAULT_HOLDOUT)
|
|
63
|
+
corpus = json.loads(Path(holdout).read_text(encoding='utf-8'))
|
|
60
64
|
catalog = [o['id'] for o in corpus['cases'][0]['request']['options']]
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
labels_file = data / ('labels-intent-scores.jsonl' if scored else 'labels-intent.jsonl' if intent else 'labels.jsonl')
|
|
66
|
+
if not labels_file.exists():
|
|
67
|
+
sys.exit(f'missing {labels_file.name}')
|
|
68
|
+
overrides = data / ('owner-overrides-intent.jsonl' if intent else 'owner-overrides.jsonl')
|
|
69
|
+
labeled = load_jsonl(labels_file)
|
|
70
|
+
gold = {r['id']: r['gold'] for r in labeled}
|
|
71
|
+
secondary = {r['id']: r.get('secondary', []) for r in labeled} if intent else {}
|
|
72
|
+
relevance = {r['id']: r['scores'] for r in labeled} if scored else {}
|
|
63
73
|
if overrides.exists():
|
|
64
|
-
|
|
74
|
+
for r in load_jsonl(overrides):
|
|
75
|
+
gold[r['id']] = r['gold']
|
|
76
|
+
if intent:
|
|
77
|
+
secondary[r['id']] = r.get('secondary', [])
|
|
78
|
+
if scored:
|
|
79
|
+
# An override without scores counts as 0.6 for its primary and 0.4 for its secondary intents.
|
|
80
|
+
relevance[r['id']] = r.get('scores') or {s: 0.6 if s == r['gold'] else 0.4 for s in [r['gold'], *r.get('secondary', [])] if s is not None}
|
|
65
81
|
excluded = data / 'owner-excluded.jsonl'
|
|
66
82
|
skip = {r['id'] for r in load_jsonl(excluded)} if excluded.exists() else set()
|
|
67
83
|
rows = [p for p in load_jsonl(data / 'prompts.jsonl') if p['id'] in gold and p['id'] not in skip]
|
|
@@ -77,34 +93,65 @@ def train(args):
|
|
|
77
93
|
|
|
78
94
|
x_train, x_val = matrix(split['train']), matrix(split['validation'])
|
|
79
95
|
y_train = torch.tensor([index(gold[r['id']]) for r in split['train']])
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
# Intent targets are soft: the primary alone gets 1.0; with secondaries it gets 0.6 and they share 0.4.
|
|
97
|
+
target = y_train
|
|
98
|
+
if scored:
|
|
99
|
+
# Scored intent labels: each intent is its own binary target with its relevance score as the soft value.
|
|
100
|
+
target = torch.tensor([[(0.6 if gold[r['id']] is None else 0.0) if c == 'unclassified' else float(relevance[r['id']].get(c, 0.0))
|
|
101
|
+
for c in catalog] for r in split['train']], dtype=torch.float32)
|
|
102
|
+
elif intent:
|
|
103
|
+
rows_t = []
|
|
104
|
+
for r in split['train']:
|
|
105
|
+
t = [0.0] * len(catalog)
|
|
106
|
+
sec = [s for s in secondary.get(r['id'], []) if s in catalog]
|
|
107
|
+
if gold[r['id']] is None or not sec:
|
|
108
|
+
t[index(gold[r['id']])] = 1.0
|
|
109
|
+
else:
|
|
110
|
+
t[index(gold[r['id']])] = 0.6
|
|
111
|
+
for s in sec:
|
|
112
|
+
t[catalog.index(s)] += 0.4 / len(sec)
|
|
113
|
+
rows_t.append(t)
|
|
114
|
+
target = torch.tensor(rows_t, dtype=torch.float32)
|
|
115
|
+
# Class mass counts the soft targets, so an intent seen only as secondary still carries weight.
|
|
116
|
+
counts = target.sum(0) if intent or scored else torch.bincount(y_train, minlength=len(catalog)).float()
|
|
117
|
+
inverse = torch.where(counts > 0, counts.sum() / (len(catalog) * counts.clamp_min(1e-6)), torch.zeros_like(counts))
|
|
82
118
|
weight = {'none': None, 'sqrt': inverse.sqrt(), 'full': inverse}[args.balance]
|
|
83
119
|
W = torch.zeros(args.dim, len(catalog), requires_grad=True)
|
|
84
120
|
bias = torch.zeros(len(catalog), requires_grad=True)
|
|
85
121
|
optimizer = torch.optim.Adam([W, bias], lr=args.lr)
|
|
86
122
|
for _ in range(args.epochs):
|
|
87
123
|
optimizer.zero_grad()
|
|
88
|
-
|
|
124
|
+
logits = torch.sparse.mm(x_train, W) + bias
|
|
125
|
+
if scored:
|
|
126
|
+
per = torch.nn.functional.binary_cross_entropy_with_logits(logits, target, reduction='none')
|
|
127
|
+
fit = (per * (weight if weight is not None else 1.0)).mean()
|
|
128
|
+
else:
|
|
129
|
+
fit = torch.nn.functional.cross_entropy(logits, target, weight=weight)
|
|
130
|
+
loss = fit + args.weight_decay * (W ** 2).sum()
|
|
89
131
|
loss.backward()
|
|
90
132
|
optimizer.step()
|
|
91
133
|
with torch.no_grad():
|
|
134
|
+
# The provider returns the softmax over the logits for either loss (docs/system-one-training.md).
|
|
92
135
|
probs = torch.softmax(torch.sparse.mm(x_val, W) + bias, 1)
|
|
93
136
|
out = Path(args.out)
|
|
94
137
|
out.mkdir(parents=True, exist_ok=True)
|
|
95
138
|
weights = W.detach().contiguous().view(-1).tolist()
|
|
96
139
|
(out / 'harness-routing-v1.bin').write_bytes(struct.pack(f'<{len(weights)}f', *weights))
|
|
97
|
-
datasets = {name: sha256_file(data / name) for name in ('prompts.jsonl',
|
|
140
|
+
datasets = {name: sha256_file(data / name) for name in ('prompts.jsonl', labels_file.name)}
|
|
98
141
|
if overrides.exists():
|
|
99
|
-
datasets[
|
|
142
|
+
datasets[overrides.name] = sha256_file(overrides)
|
|
100
143
|
sidecar = {'format': 'harness-ngram', 'formatVersion': 1, 'dim': args.dim, 'nmax': args.nmax, 'hash': 'fnv1a32', 'catalog': catalog,
|
|
101
144
|
'bias': [float(v) for v in bias.detach().tolist()],
|
|
102
|
-
'metadata': {'domain': 'harness-routing-v1', 'balance': args.balance, 'weightDecay': args.weight_decay, 'epochs': args.epochs,
|
|
103
|
-
'seed': args.seed, 'datasets': datasets, 'counts': {k: len(v) for k, v in split.items()}
|
|
145
|
+
'metadata': {'domain': 'harness-routing-v1', 'task': args.task, 'balance': args.balance, 'weightDecay': args.weight_decay, 'epochs': args.epochs,
|
|
146
|
+
'seed': args.seed, 'datasets': datasets, 'counts': {k: len(v) for k, v in split.items()},
|
|
147
|
+
**({'target': 'scores', 'loss': 'bce'} if scored else {})}}
|
|
104
148
|
(out / 'harness-routing-v1.json').write_text(json.dumps(sidecar), encoding='utf-8')
|
|
105
149
|
with (out / 'val-scores.jsonl').open('w', encoding='utf-8') as fh:
|
|
106
150
|
for r, p in zip(split['validation'], probs.tolist()):
|
|
107
|
-
|
|
151
|
+
row = {'id': r['id'], 'gold': gold[r['id']], 'probs': p}
|
|
152
|
+
if intent:
|
|
153
|
+
row['secondary'] = secondary.get(r['id'], [])
|
|
154
|
+
fh.write(json.dumps(row) + '\n')
|
|
108
155
|
y_val = torch.tensor([index(gold[r['id']]) for r in split['validation']])
|
|
109
156
|
accuracy = (probs.argmax(1) == y_val).float().mean().item() if len(y_val) else 0.0
|
|
110
157
|
print(json.dumps({'counts': sidecar['metadata']['counts'], 'validationAccuracy': accuracy}))
|
|
@@ -114,7 +161,9 @@ def main(argv=None):
|
|
|
114
161
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
115
162
|
parser.add_argument('--data-dir', required=True)
|
|
116
163
|
parser.add_argument('--out', required=True)
|
|
117
|
-
parser.add_argument('--
|
|
164
|
+
parser.add_argument('--task', choices=['tier', 'intent'], default='tier')
|
|
165
|
+
parser.add_argument('--target', choices=['label', 'scores'], default='label', help='scores: per-intent relevance targets from labels-intent-scores.jsonl (intent only)')
|
|
166
|
+
parser.add_argument('--holdout', default=None, help='source of the fixed catalog only (defaults to the task holdout); holdout rows are never trained on')
|
|
118
167
|
parser.add_argument('--dim', type=int, default=1 << 16)
|
|
119
168
|
parser.add_argument('--nmax', type=int, default=3)
|
|
120
169
|
parser.add_argument('--balance', choices=['none', 'sqrt', 'full'], default='sqrt')
|
|
@@ -122,7 +171,10 @@ def main(argv=None):
|
|
|
122
171
|
parser.add_argument('--epochs', type=int, default=60)
|
|
123
172
|
parser.add_argument('--lr', type=float, default=0.01)
|
|
124
173
|
parser.add_argument('--seed', type=int, default=7)
|
|
125
|
-
|
|
174
|
+
args = parser.parse_args(argv)
|
|
175
|
+
if args.target == 'scores' and args.task != 'intent':
|
|
176
|
+
parser.error('--target scores needs --task intent')
|
|
177
|
+
train(args)
|
|
126
178
|
|
|
127
179
|
|
|
128
180
|
if __name__ == '__main__':
|