File size: 9,868 Bytes
6fd3740 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | """Datasets, collation (random mask ratio = masked-diffusion training), splits, negatives."""
from __future__ import annotations
import json
import random
import torch
from .features import build_pass_a, build_pass_b, gold_plan
from .schema import Example, example_from_dict, example_to_dict
def load_jsonl(path) -> list[Example]:
with open(path, encoding="utf-8") as f:
return [example_from_dict(json.loads(line)) for line in f if line.strip()]
def save_jsonl(path, examples: list[Example]):
with open(path, "w", encoding="utf-8") as f:
for ex in examples:
f.write(json.dumps(example_to_dict(ex), ensure_ascii=False) + "\n")
def featurize(tok, ex: Example, cached: bool = False, markers: bool = False, v3: bool = False) -> dict | None:
if v3:
from .v3feat import build_pass_a_v3, build_pass_b_v3, gold_plan_v3
a = build_pass_a_v3(tok, ex)
if a is None:
return None
b = None
if ex.calls and len(ex.calls) <= len(a["call_pos"]):
b = build_pass_b_v3(tok, ex, a, gold_plan_v3(tok, ex, a), with_targets=True, keep_context=cached,
markers=markers)
return {"a": a, "b": b}
a = build_pass_a(tok, ex)
if a is None:
return None
b = None
if ex.calls and len(ex.calls) <= len(a["call_pos"]):
b = build_pass_b(tok, ex, a, gold_plan(a, ex), with_targets=True, keep_context=cached, markers=markers)
return {"a": a, "b": b}
class FeatureDataset(torch.utils.data.Dataset):
def __init__(self, tok, examples: list[Example], cached: bool = False, markers: bool = False, v3: bool = False):
self.tok, self.examples, self.cached, self.markers, self.v3 = tok, examples, cached, markers, v3
def __len__(self):
return len(self.examples)
def __getitem__(self, i):
return featurize(self.tok, self.examples[i], self.cached, self.markers, self.v3)
def length(self, i) -> int:
"""Cheap length proxy for bucketing (chars/3)."""
ex = self.examples[i]
n = len(ex.query) + sum(len(t.name) + len(t.description) + 20 * len(t.params) for t in ex.tools)
return n // 3 + 40
def _pad(seqs, value):
L = max(len(s) for s in seqs)
ids = torch.full((len(seqs), L), value, dtype=torch.long)
attn = torch.zeros((len(seqs), L), dtype=torch.long)
for i, s in enumerate(seqs):
ids[i, : len(s)] = torch.tensor(s)
attn[i, : len(s)] = 1
return ids, attn
def _mask_slots(b, rng, v3_mask: str = "clean"):
"""Masked-diffusion corruption: random ratio r ~ U(0,1], mask that share of slot tokens, reveal the rest.
v3_mask="legacy" reproduces the original v3 run (s10): every anchor always masked, gate trained on all anchors."""
ids = list(b["input_ids"])
flat = [(p, t) for pos, tg in zip(b["slot_pos"], b["slot_targets"]) for p, t in zip(pos, tg)]
if not flat: # v3: option-only slot block
return ids, []
n_mask = max(1, round(rng.uniform(0.0, 1.0) * len(flat)))
masked = set(rng.sample(range(len(flat)), n_mask))
always = set()
if b.get("anchors") and v3_mask == "legacy":
always = set(b["anchors"])
b.pop("_clean", None)
elif b.get("anchors"):
# v3: half the values are fully masked ("clean"); only their anchors train the copy gate / span pointer,
# matching inference, where the gate is read once with every slot masked. Other values follow the random
# mask, so revealed anchors (as in later refinement passes) are seen too.
clean = [pos for pos in b["slot_pos"] if rng.random() < 0.5]
always = {p for pos in clean for p in pos}
b["_clean"] = [pos[0] for pos in clean]
targets = []
for idx, (p, t) in enumerate(flat):
if idx in masked or p in always:
targets.append((p, t))
else:
ids[p] = t
return ids, targets
def collate(items, tok, rng: random.Random | None = None, cached: bool = False, v3_mask: str = "clean"):
rng = rng or random
items = [it for it in items if it is not None]
if not items:
return None
if cached:
return _collate_cached(items, tok, rng, v3_mask)
A = [it["a"] for it in items]
a_ids, a_attn = _pad([a["input_ids"] for a in A], tok.pad_token_id)
J = max(max(len(a["t_pos"]) for a in A), 1)
t_pos = torch.full((len(A), J), -1, dtype=torch.long)
for i, a in enumerate(A):
t_pos[i, : len(a["t_pos"])] = torch.tensor(a["t_pos"], dtype=torch.long)
dec = torch.tensor([[J if (l == len(a["t_pos"])) else l for l in a["dec_labels"]] for a in A])
len_idx, len_labels = [], []
for i, a in enumerate(A):
for (ci, j, k, b) in a["len_items"]:
len_idx.append((i, a["call_pos"][ci], a["arg_pos"][(j, k)]))
len_labels.append(b)
batch = {"a_ids": a_ids, "a_attn": a_attn, "t_pos": t_pos, "t_mask": t_pos >= 0,
"call_pos": torch.tensor([a["call_pos"] for a in A]), "dec_labels": dec,
"len_idx": torch.tensor(len_idx, dtype=torch.long).reshape(-1, 3),
"len_labels": torch.tensor(len_labels, dtype=torch.long)}
# pass B with a random mask ratio per example (masked-diffusion objective)
B = [it["b"] for it in items if it["b"] is not None and (it["b"]["slot_pos"] or it["b"].get("opt_slots"))]
if B:
seqs, sb, sp, sl = [], [], [], []
for bi, b in enumerate(B):
ids, targets = _mask_slots(b, rng, v3_mask)
for p, t in targets:
sb.append(bi); sp.append(p); sl.append(t)
seqs.append(ids)
b_ids, b_attn = _pad(seqs, tok.pad_token_id)
batch.update({"b_ids": b_ids, "b_attn": b_attn, "slot_b": torch.tensor(sb), "slot_p": torch.tensor(sp),
"slot_labels": torch.tensor(sl)})
else:
batch["b_ids"] = None
if "q_pos" in A[0]:
from .v3feat import v3_extras
batch.update(v3_extras(A, list(enumerate(B))))
return batch
def _collate_cached(items, tok, rng, v3_mask="clean"):
"""One sequence per example: pass-A context + slot block, read with the block-causal mask."""
A, seqs, ctx, sb, sp, sl = [], [], [], [], [], []
for i, it in enumerate(items):
a, b = it["a"], it["b"]
A.append(a)
ctx.append(len(a["input_ids"]))
if b is not None and (b["slot_pos"] or b.get("opt_slots")):
ids, targets = _mask_slots(b, rng, v3_mask)
for p, t in targets:
sb.append(i); sp.append(p); sl.append(t)
seqs.append(ids)
else:
seqs.append(a["input_ids"])
ids, attn = _pad(seqs, tok.pad_token_id)
J = max(max(len(a["t_pos"]) for a in A), 1)
t_pos = torch.full((len(A), J), -1, dtype=torch.long)
for i, a in enumerate(A):
t_pos[i, : len(a["t_pos"])] = torch.tensor(a["t_pos"], dtype=torch.long)
dec = torch.tensor([[J if (l == len(a["t_pos"])) else l for l in a["dec_labels"]] for a in A])
len_idx, len_labels = [], []
for i, a in enumerate(A):
for (ci, j, k, b) in a["len_items"]:
len_idx.append((i, a["call_pos"][ci], a["arg_pos"][(j, k)]))
len_labels.append(b)
out = {"a_ids": ids, "a_attn": attn, "t_pos": t_pos, "t_mask": t_pos >= 0,
"call_pos": torch.tensor([a["call_pos"] for a in A]), "dec_labels": dec,
"len_idx": torch.tensor(len_idx, dtype=torch.long).reshape(-1, 3),
"len_labels": torch.tensor(len_labels, dtype=torch.long),
"ctx_len": torch.tensor(ctx), "b_ids": None, "slot_b": torch.tensor(sb, dtype=torch.long),
"slot_p": torch.tensor(sp, dtype=torch.long), "slot_labels": torch.tensor(sl, dtype=torch.long)}
if "q_pos" in A[0]:
from .v3feat import v3_extras
out.update(v3_extras(A, [(i, it["b"]) for i, it in enumerate(items)
if it["b"] is not None and (it["b"]["slot_pos"] or it["b"].get("opt_slots"))]))
return out
def token_batches(lengths: list[int], tokens_per_batch: int, rng: random.Random, max_bs: int = 256):
"""Length-bucketed batches (sorted by length, chunked by token budget, shuffled)."""
order = sorted(range(len(lengths)), key=lambda i: lengths[i])
batches, cur, cur_max = [], [], 0
for i in order:
m = max(cur_max, lengths[i])
if cur and (m * (len(cur) + 1) > tokens_per_batch or len(cur) >= max_bs):
batches.append(cur); cur, m = [], lengths[i]
cur.append(i); cur_max = m
if cur:
batches.append(cur)
rng.shuffle(batches)
return batches
def hard_negatives(examples: list[Example], n: int, rng: random.Random) -> list[Example]:
"""Same query, same domain: drop every tool the gold answer uses, keep its related neighbours -> no call."""
out = []
pool = [e for e in examples if e.calls and len(e.tools) > len({c.name for c in e.calls})]
rng.shuffle(pool)
for e in pool[:n]:
needed = {c.name for c in e.calls}
tools = [t for t in e.tools if t.name not in needed]
out.append(Example(e.query, tools, [], id=f"hardneg_{len(out)}", source="xlam_hardneg", gold=[]))
return out
def irrelevance_negatives(examples: list[Example], n: int, rng: random.Random) -> list[Example]:
"""Pair real queries with tool lists from other examples that don't contain any needed tool."""
out = []
pool = [e for e in examples if e.tools]
while len(out) < n:
q, other = rng.choice(pool), rng.choice(pool)
needed = {c.name for c in q.calls}
tools = [t for t in other.tools if t.name not in needed]
if not tools or other is q:
continue
out.append(Example(q.query, tools, [], id=f"neg_{len(out)}", source="xlam_neg", gold=[]))
return out
|