@hviana/sema 0.5.7 → 0.5.9
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/AGENTS.md +23 -0
- package/DATASETS.md +159 -0
- package/HOW_IT_WORKS.md +74 -0
- package/README.md +12 -0
- package/dist/example/train_base.d.ts +73 -3
- package/dist/example/train_base.js +1000 -49
- package/dist/src/geometry.d.ts +20 -0
- package/dist/src/geometry.js +22 -0
- package/dist/src/mind/articulation.js +15 -2
- package/dist/src/mind/attention.d.ts +6 -0
- package/dist/src/mind/attention.js +44 -4
- package/dist/src/mind/learning.js +250 -3
- package/dist/src/mind/mechanisms/cast.js +45 -1
- package/dist/src/mind/mind.d.ts +6 -1
- package/dist/src/mind/mind.js +14 -2
- package/dist/src/mind/reasoning.js +59 -5
- package/dist/src/mind/recognition.js +29 -3
- package/dist/src/mind/traverse.d.ts +34 -0
- package/dist/src/mind/traverse.js +42 -0
- package/dist/src/store-sqlite.d.ts +4 -0
- package/dist/src/store-sqlite.js +47 -0
- package/dist/src/store.d.ts +7 -0
- package/example/train_base.ts +1193 -46
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/geometry.ts +23 -0
- package/src/mind/articulation.ts +16 -2
- package/src/mind/attention.ts +54 -1
- package/src/mind/learning.ts +253 -4
- package/src/mind/mechanisms/cast.ts +48 -1
- package/src/mind/mind.ts +12 -1
- package/src/mind/reasoning.ts +64 -5
- package/src/mind/recognition.ts +29 -3
- package/src/mind/traverse.ts +48 -0
- package/src/store-sqlite.ts +53 -0
- package/src/store.ts +28 -0
- package/test/29-counterfactual.test.mjs +43 -6
- package/test/76-type-level-company.test.mjs +342 -0
- package/test/77-company-saturation.test.mjs +302 -0
- package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
- package/test/84-composed-answer-honesty.test.mjs +136 -0
- package/test/85-answered-directly.test.mjs +126 -0
- package/test/86-cast-voices-committed.test.mjs +164 -0
- package/test/87-codominant-commitment.test.mjs +250 -0
package/example/train_base.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
//This file uses the Google SMOL dataset, made available under the CC BY 4.0 license.
|
|
2
2
|
//This file uses Aya and oasst2 datasets, made available under the apache-2.0 license.
|
|
3
|
-
//
|
|
3
|
+
//
|
|
4
|
+
//A trained Sema store retains its training text VERBATIM, so distributing a
|
|
5
|
+
//store distributes these corpora and every upstream licence applies to it in
|
|
6
|
+
//full. Read DATASETS.md before adding a corpus here or publishing a store:
|
|
7
|
+
//it carries the per-corpus attribution a distributed store is required to
|
|
8
|
+
//travel with, and the two rules a candidate corpus must pass (no NonCommercial
|
|
9
|
+
//term, no ShareAlike term — checked against what the corpus was BUILT FROM,
|
|
10
|
+
//not merely against the repository's licence tag).
|
|
4
11
|
|
|
5
12
|
//This file is a more appropriate training example for Sema.
|
|
6
13
|
//Sema does not learn through repetition;
|
|
@@ -21,23 +28,42 @@
|
|
|
21
28
|
//
|
|
22
29
|
// Every source here is commercially licensable (cc-by-4.0 / apache-2.0).
|
|
23
30
|
//
|
|
24
|
-
// The curriculum runs in
|
|
31
|
+
// The curriculum runs in eight stages, into ONE store:
|
|
25
32
|
// 1. SmolSent (google/smol) — sentence-level TRANSLATION pairs across 100+
|
|
26
33
|
// low-resource languages; see §6c. Each pair is "two names for one meaning"
|
|
27
|
-
// →
|
|
28
|
-
//
|
|
34
|
+
// → a foreign→English translation FACT, so every language's rendering of a
|
|
35
|
+
// meaning converges on ONE English node — the cross-language concept SEMA
|
|
36
|
+
// fuses (cf. test/05-concepts.test.mjs). The reverse binding is NOT
|
|
37
|
+
// deposited by default; see SMOLSENT_DIRECTIONS for why.
|
|
29
38
|
// 2. Aya Dataset — ~204k human prompt→completion pairs, 70+ languages; see §6d
|
|
30
39
|
// → one (question → answer) FACT each.
|
|
31
40
|
// 3. oasst2 — MULTI-TURN human↔assistant conversation trees; see §6e → the
|
|
32
41
|
// accumulated-context walk (single-turn trees are skipped, by design).
|
|
33
|
-
// 4.
|
|
34
|
-
//
|
|
42
|
+
// 4. Taskmaster 1–4 (google-research-datasets) — task-oriented DIALOGUE, the
|
|
43
|
+
// best-scoring corpora on the fold-unit recurrence benchmark that predicts
|
|
44
|
+
// halo health; see §6e′ → the accumulated-context walk, over turns merged
|
|
45
|
+
// per speaker.
|
|
46
|
+
// 5. 2WikiMultihopQA — the `evidences` (subject, relation, object) TRIPLES,
|
|
47
|
+
// the one stage aimed at COMPOSITION; see §6e″ → a relation fact plus a
|
|
48
|
+
// bare-subject PIVOT fact each. Its Wikipedia passages and its composed
|
|
49
|
+
// questions are deliberately NOT read.
|
|
50
|
+
// 6. SODA — social/commonsense DIALOGUE; see §6e‴ → the accumulated-context
|
|
51
|
+
// walk. Budgeted: its train split alone would otherwise contribute ~8M
|
|
52
|
+
// episodes against 662k for the whole current corpus.
|
|
53
|
+
// 7. MASSIVE — short intent utterances in 51 locales; see §6e⁗ → ONE bare
|
|
54
|
+
// experience each. Contributes recurring fold units, nothing relational.
|
|
55
|
+
// DISABLED BY DEFAULT — edge-less content was measured to manufacture
|
|
56
|
+
// answers where the store should stay silent.
|
|
57
|
+
// 8. General-Knowledge (MuskumPillerum) — ~37.6k {Question, Answer} pairs; see
|
|
58
|
+
// §6f → one (question → answer) FACT each. DISABLED BY DEFAULT on licence
|
|
59
|
+
// grounds; see DATASETS.md §3.2.
|
|
35
60
|
// Each stage runs only after the previous one finishes, and is recorded in the
|
|
36
61
|
// same completed-files set, so a single store resumes the whole curriculum.
|
|
37
62
|
//
|
|
38
63
|
// Every source is DOWNLOADED as a file and streamed from disk (never paged
|
|
39
64
|
// row-by-row over an HTTP API — that was slow and rate-limited): SmolSent as
|
|
40
|
-
// per-pair JSONL, oasst2 as a gzipped JSONL, General-Knowledge as
|
|
65
|
+
// per-pair JSONL, oasst2 as a gzipped JSONL, Taskmaster and General-Knowledge as
|
|
66
|
+
// JSON arrays,
|
|
41
67
|
// and Aya as Snappy-Parquet read row-group by row-group with hyparquet (the one
|
|
42
68
|
// case the web platform can't decode alone). Resume is per-file: a fully-
|
|
43
69
|
// consumed file is marked complete; an interrupted one re-reads from the top
|
|
@@ -86,12 +112,21 @@
|
|
|
86
112
|
// MAX_MB=500 node dist/example/train_base.js
|
|
87
113
|
// CHECKPOINT_MB=250 node dist/example/train_base.js
|
|
88
114
|
// SMOLSENT_PAIRS=ha_en,zu_en node dist/example/train_base.js # a subset of pairs
|
|
115
|
+
// SMOLSENT_DIRECTIONS=both node dist/example/train_base.js # also English->foreign
|
|
89
116
|
// SMOLSENT=0 node dist/example/train_base.js # skip SmolSent stage
|
|
90
117
|
// AYA=0 node dist/example/train_base.js # skip Aya stage
|
|
91
118
|
// AYA_SPLIT=test node dist/example/train_base.js # small Aya slice
|
|
92
119
|
// OASST=0 node dist/example/train_base.js # skip oasst2 stage
|
|
93
120
|
// OASST_MIN_TURNS=6 node dist/example/train_base.js # deeper multi-turn only
|
|
94
|
-
// GENKNOW=
|
|
121
|
+
// GENKNOW=1 node dist/example/train_base.js # General-Knowledge (see DATASETS.md §3.2)
|
|
122
|
+
// PARQUET_BATCH_MB=8 node dist/example/train_base.js # smaller Parquet reads on a tight host
|
|
123
|
+
// TASKMASTER=0 node dist/example/train_base.js # skip Taskmaster stage
|
|
124
|
+
// TASKMASTER_SETS=TM-3-2020 node dist/example/train_base.js # one Taskmaster set
|
|
125
|
+
// WIKI2=0 node dist/example/train_base.js # skip 2Wiki triples stage
|
|
126
|
+
// SODA=0 node dist/example/train_base.js # skip the SODA stage
|
|
127
|
+
// MASSIVE=1 node dist/example/train_base.js # enable MASSIVE (off by default)
|
|
128
|
+
// SODA_MAX_DIALOGS=0 node dist/example/train_base.js # lift the SODA budget
|
|
129
|
+
// WIKI2_MAX_ROWS=50000 node dist/example/train_base.js # budget the 2Wiki stage
|
|
95
130
|
// LOCAL_PATH=./base node dist/example/train_base.js # offline: *.jsonl/.parquet/.jsonl.gz/.json
|
|
96
131
|
// DB_PATH=./data/sema node dist/example/train_base.js
|
|
97
132
|
|
|
@@ -153,6 +188,40 @@ const SMOLSENT_PAIRS = (process.env.SMOLSENT_PAIRS ?? "")
|
|
|
153
188
|
// The resume id PREFIX for the SmolSent stage; one completed-files entry per
|
|
154
189
|
// file (e.g. "smolsent::ha_en.jsonl").
|
|
155
190
|
const SMOLSENT_ID = "smolsent";
|
|
191
|
+
// Which direction(s) of a translation pair to deposit. Was effectively "both",
|
|
192
|
+
// and that is now the default NO longer, for a reason measured rather than
|
|
193
|
+
// assumed.
|
|
194
|
+
//
|
|
195
|
+
// SmolSent's English side is a SHARED POOL translated into every language: row
|
|
196
|
+
// id 0 of smolsent/ha_en.jsonl, zu_en.jsonl and am_en.jsonl all carry the SAME
|
|
197
|
+
// `trg` ("It allows me to work by following my vibes and ..."). The two
|
|
198
|
+
// directions are therefore not symmetric at all:
|
|
199
|
+
//
|
|
200
|
+
// src2trg (foreign -> English) many distinct contexts -> ONE shared
|
|
201
|
+
// continuation. Every language's rendering of
|
|
202
|
+
// a meaning converges on the same English
|
|
203
|
+
// node — the cross-language concept fusion
|
|
204
|
+
// this stage exists for.
|
|
205
|
+
// trg2src (English -> foreign) ONE context -> 100+ DIFFERENT continuations,
|
|
206
|
+
// one per language file. The same English
|
|
207
|
+
// sentence is deposited over and over with a
|
|
208
|
+
// different answer each time.
|
|
209
|
+
//
|
|
210
|
+
// So dropping trg2src is not merely a corpus-size economy (it halves the
|
|
211
|
+
// largest stage, which was 60.9% of all examples in the last trained store); it
|
|
212
|
+
// removes a genuine ambiguity pathology. Set SMOLSENT_DIRECTIONS=both to
|
|
213
|
+
// restore the old behaviour, or trg2src for English->foreign only.
|
|
214
|
+
//
|
|
215
|
+
// WHAT THE CUT DOES NOT DO, measured on a three-pair store: asking the English
|
|
216
|
+
// sentence still ANSWERS with a foreign rendering, because the engine can reach
|
|
217
|
+
// a shared continuation's predecessors on its own. What is removed is the
|
|
218
|
+
// DEPOSITED forward ambiguity — one context carrying ~100 competing
|
|
219
|
+
// continuations — not every reverse association.
|
|
220
|
+
const SMOLSENT_DIRECTIONS = env("SMOLSENT_DIRECTIONS", "src2trg")
|
|
221
|
+
.trim().toLowerCase();
|
|
222
|
+
const SMOLSENT_SRC2TRG = SMOLSENT_DIRECTIONS !== "trg2src";
|
|
223
|
+
const SMOLSENT_TRG2SRC = SMOLSENT_DIRECTIONS === "trg2src" ||
|
|
224
|
+
SMOLSENT_DIRECTIONS === "both";
|
|
156
225
|
// A SmolSent side longer than this is skipped (a sentence pair is short; a huge
|
|
157
226
|
// value is corruption, not a sentence).
|
|
158
227
|
const MAX_SMOLSENT_CHARS = Math.max(
|
|
@@ -173,6 +242,18 @@ const CHECKPOINT_BYTES = Math.max(
|
|
|
173
242
|
1_000_000,
|
|
174
243
|
Math.floor(Number(env("CHECKPOINT_MB", "100")) * 1_000_000) || 100_000_000,
|
|
175
244
|
);
|
|
245
|
+
// Target size of ONE materialised Parquet read, in uncompressed source bytes.
|
|
246
|
+
// A row-GROUP is a layout choice made by whoever wrote the file, not a memory
|
|
247
|
+
// budget: Aya ships 203 groups of 1,000 rows (~1 MB each), while SODA ships ONE
|
|
248
|
+
// group of 1,191,582 rows (1.19 GB uncompressed) and 2Wiki ONE of 167,454
|
|
249
|
+
// (666 MB). Reading "exactly one row-group" is therefore safe for the first and
|
|
250
|
+
// fatal for the others, so reads are sized in BYTES instead — see
|
|
251
|
+
// `parquetBatchRows`. Materialised JS objects cost several times their source
|
|
252
|
+
// bytes, hence a default well under available memory.
|
|
253
|
+
const PARQUET_BATCH_BYTES = Math.max(
|
|
254
|
+
1_000_000,
|
|
255
|
+
Math.floor(Number(env("PARQUET_BATCH_MB", "32")) * 1_000_000) || 32_000_000,
|
|
256
|
+
);
|
|
176
257
|
const LOCAL_PATH = env("LOCAL_PATH", ""); // train from a local dir of *.zip
|
|
177
258
|
const CACHE_DIR = env("CACHE_DIR", join(process.cwd(), "cache"));
|
|
178
259
|
const MAX_CACHE_BYTES = Number(env("MAX_CACHE_GB", "100")) * 1e9;
|
|
@@ -276,13 +357,187 @@ const MAX_OASST_LINE_CHARS = Math.max(
|
|
|
276
357
|
Math.floor(Number(env("MAX_OASST_LINE_MB", "8")) * 1_000_000) || 8_000_000,
|
|
277
358
|
);
|
|
278
359
|
|
|
360
|
+
// ── google-research-datasets/Taskmaster 1–4 (the dialogue stages) ──
|
|
361
|
+
// Four corpora of task-oriented dialogue, one shape between them: each file is a
|
|
362
|
+
// JSON ARRAY of conversations and each conversation carries
|
|
363
|
+
// `utterances: [{speaker, text, …}]`. TM-1 ships two files directly under its
|
|
364
|
+
// directory (self-dialogs, woz-dialogs); TM-2/3/4 ship theirs under `<set>/data`.
|
|
365
|
+
// They are the best-scoring corpora on the fold-unit recurrence benchmark that
|
|
366
|
+
// selects for halo health (TM-3 85.1%, TM-4 78.8%, TM-2 68.7%, TM-1 51.8%,
|
|
367
|
+
// against 23.2% for the incumbent SmolSent), and they are genuinely multi-turn
|
|
368
|
+
// where the incumbent multi-turn stage is not (TM-3 median 20 turns of ~43 B,
|
|
369
|
+
// against oasst2's median turn of 529 B).
|
|
370
|
+
//
|
|
371
|
+
// Served from GitHub raw, not Hugging Face: the HF mirrors are loading-script
|
|
372
|
+
// repos with no data files, and the official copies carry the CC BY 4.0 notice.
|
|
373
|
+
const TASKMASTER = env("TASKMASTER", "1") !== "0";
|
|
374
|
+
// Which sets to train, in order. Each is a directory in the Taskmaster repo.
|
|
375
|
+
const TASKMASTER_SETS = env(
|
|
376
|
+
"TASKMASTER_SETS",
|
|
377
|
+
"TM-1-2019,TM-2-2020,TM-3-2020,TM-4-2024",
|
|
378
|
+
).split(",").map((s) => s.trim()).filter(Boolean);
|
|
379
|
+
const TASKMASTER_REPO = env(
|
|
380
|
+
"TASKMASTER_REPO",
|
|
381
|
+
"google-research-datasets/Taskmaster",
|
|
382
|
+
);
|
|
383
|
+
const TASKMASTER_RAW =
|
|
384
|
+
`https://raw.githubusercontent.com/${TASKMASTER_REPO}/master`;
|
|
385
|
+
// A conversation must have at least this many turns AFTER same-speaker merging.
|
|
386
|
+
// The default of 2 keeps every real exchange: unlike oasst2 — where a lone Q→A
|
|
387
|
+
// tree merely replicates the Aya stage's shape and is dropped — a two-turn
|
|
388
|
+
// task-oriented exchange is still task-oriented dialogue, and TM-4's dialogues
|
|
389
|
+
// are short by design (median 3.7 turns), so a higher bar would discard most of
|
|
390
|
+
// that set.
|
|
391
|
+
const TASKMASTER_MIN_TURNS = Math.max(
|
|
392
|
+
2,
|
|
393
|
+
Math.floor(Number(env("TASKMASTER_MIN_TURNS", "2"))) || 2,
|
|
394
|
+
);
|
|
395
|
+
// Skip a conversation carrying an implausibly long utterance (corruption). The
|
|
396
|
+
// measured maximum across TM-1/2/3/4 is 1,897 bytes, so this only guards.
|
|
397
|
+
const MAX_TASKMASTER_TURN_CHARS = Math.max(
|
|
398
|
+
1_000,
|
|
399
|
+
Math.floor(Number(env("MAX_TASKMASTER_TURN_KB", "32")) * 1000) || 32_000,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// ── 2WikiMultihopQA — the `evidences` TRIPLES only (the composition stage) ──
|
|
403
|
+
// Each row carries `evidences`: a JSON string of (subject, relation, object)
|
|
404
|
+
// triples that CHAIN — one triple's object is the next's subject. 72.5% of rows
|
|
405
|
+
// carry such a chain (measured over 4,000 rows), and those triples are the only
|
|
406
|
+
// representation measured to make Sema compose a two-hop answer at all.
|
|
407
|
+
//
|
|
408
|
+
// TWO COLUMNS ARE DELIBERATELY NOT READ, one for licence reasons and one for
|
|
409
|
+
// capability reasons:
|
|
410
|
+
// • `context` holds Wikipedia PROSE. The repo is Apache-2.0 but Wikipedia text
|
|
411
|
+
// is CC BY-SA, and a Sema store keeps text verbatim, so ingesting the
|
|
412
|
+
// passages would attach ShareAlike to every distributed store. The triples
|
|
413
|
+
// originate in Wikidata (CC0). See DATASETS.md §3.2/§4.
|
|
414
|
+
// • `question`/`answer` are the composed multi-hop QUESTION. Depositing those
|
|
415
|
+
// teaches the answer to that exact question and nothing else — it memorises
|
|
416
|
+
// rather than composes. They are used to EVALUATE this adapter, never as
|
|
417
|
+
// training input.
|
|
418
|
+
//
|
|
419
|
+
// Read from Hugging Face's auto-converted `refs/convert/parquet` branch, not
|
|
420
|
+
// from main: the main-branch train.parquet is written as ONE 167,454-row
|
|
421
|
+
// group (666 MB uncompressed) and a Parquet column chunk is per-group, so any
|
|
422
|
+
// read of it materialises the whole file. The converted branch uses uniform
|
|
423
|
+
// 10,000-row groups. See test/79-parquet-batching.test.mjs.
|
|
424
|
+
const WIKI2 = env("WIKI2", "1") !== "0";
|
|
425
|
+
const WIKI2_DATASET = env("WIKI2_DATASET", "xanhho/2WikiMultihopQA");
|
|
426
|
+
// Splits to train, in order. Only `train` by default: `validation`/`test` are
|
|
427
|
+
// the dataset's held-out sets and are what an honest evaluation of this
|
|
428
|
+
// adapter's composition rate has to be measured on.
|
|
429
|
+
const WIKI2_SPLITS = env("WIKI2_SPLITS", "train")
|
|
430
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
431
|
+
// Reject a triple with an implausibly long field (corruption); real subjects and
|
|
432
|
+
// objects are entity names, and relations are Wikidata property labels.
|
|
433
|
+
// 0 = every row. The train split holds 167,454 rows at ~4.95 deposits each
|
|
434
|
+
// (~830k facts), so this is the knob that keeps 2Wiki proportionate to the rest
|
|
435
|
+
// of the curriculum in the same way SODA_MAX_DIALOGS does.
|
|
436
|
+
const WIKI2_MAX_ROWS = Math.max(
|
|
437
|
+
0,
|
|
438
|
+
Math.floor(Number(env("WIKI2_MAX_ROWS", "0"))) || 0,
|
|
439
|
+
);
|
|
440
|
+
const MAX_WIKI2_FIELD_CHARS = Math.max(
|
|
441
|
+
100,
|
|
442
|
+
Math.floor(Number(env("MAX_WIKI2_FIELD_KB", "2")) * 1000) || 2_000,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// ── allenai/soda (social dialogue) and AmazonScience/massive (short intents) ──
|
|
446
|
+
// Both are read from Hugging Face's auto-converted `refs/convert/parquet`
|
|
447
|
+
// branch. For SODA that is mandatory, not cosmetic: its main-branch
|
|
448
|
+
// train.parquet is ONE 1,191,582-row group (1.19 GB uncompressed), and a
|
|
449
|
+
// Parquet column chunk is per-group, so any read of it materialises the whole
|
|
450
|
+
// file — measured at 100% of a 689 MB file and 2 GB of heap for a 500-row read.
|
|
451
|
+
// The converted branch uses uniform 10,000-row groups.
|
|
452
|
+
//
|
|
453
|
+
// BOTH STAGES ARE BUDGETED, and that is a curriculum decision rather than an
|
|
454
|
+
// algorithmic cap. SODA's train split holds 1,191,582 dialogues which the
|
|
455
|
+
// cumulative walk would turn into ~8 MILLION episodes — against the 662,221
|
|
456
|
+
// deposits of the entire current corpus. Trained whole it would not join the
|
|
457
|
+
// mix, it would BE the mix, and corpus size is the quantity every scale problem
|
|
458
|
+
// in this engine is measured against. The default takes the first
|
|
459
|
+
// SODA_MAX_DIALOGS of them; set it to 0 to lift the budget.
|
|
460
|
+
const SODA = env("SODA", "1") !== "0";
|
|
461
|
+
const SODA_DATASET = env("SODA_DATASET", "allenai/soda");
|
|
462
|
+
const SODA_SPLITS = env("SODA_SPLITS", "train")
|
|
463
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
464
|
+
// ~6.3 episodes per dialogue, so this budgets ~750k episodes — comparable to
|
|
465
|
+
// the Taskmaster stage and to Aya, which is the intended balance. 0 = no budget.
|
|
466
|
+
const SODA_MAX_DIALOGS = Math.max(
|
|
467
|
+
0,
|
|
468
|
+
Math.floor(Number(env("SODA_MAX_DIALOGS", "120000"))) || 0,
|
|
469
|
+
);
|
|
470
|
+
const MAX_SODA_TURN_CHARS = Math.max(
|
|
471
|
+
1_000,
|
|
472
|
+
Math.floor(Number(env("MAX_SODA_TURN_KB", "32")) * 1000) || 32_000,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// MASSIVE deposits BARE UTTERANCES — an experience, not an episode — and that
|
|
476
|
+
// is the only shape its data supports. Two richer shapes were considered and
|
|
477
|
+
// rejected on evidence:
|
|
478
|
+
// • Same-intent pairs as paraphrases. 49.1% of consecutive rows share
|
|
479
|
+
// (locale, intent), but they are NOT meaning-equivalent: intent 48 in mn-MN
|
|
480
|
+
// runs "wake me at nine on the fifth" next to "set an alarm two hours from
|
|
481
|
+
// now". Depositing that pair as an episode teaches a continuation that does
|
|
482
|
+
// not exist.
|
|
483
|
+
// • Same-id rows across locales. Those ARE translations of one another —
|
|
484
|
+
// which is exactly SmolSent's relation, and SmolSent scores worst of every
|
|
485
|
+
// corpus measured on fold-unit recurrence (23.2%) because cross-lingual
|
|
486
|
+
// pairs share no units.
|
|
487
|
+
// So the stage contributes recurring fold units and lexical coverage (65.1%
|
|
488
|
+
// recurring unit mass, median 29 B) and nothing relational. `annot_utt` carries
|
|
489
|
+
// slot markup ("[date : tavdahad] ...") and is never read.
|
|
490
|
+
// DISABLED BY DEFAULT, on evidence gathered after the stage was written. A bare
|
|
491
|
+
// experience deposits content with NO EDGE, and that cuts both ways. Measured on
|
|
492
|
+
// a three-pair dialogue store with and without six MASSIVE-style utterances:
|
|
493
|
+
//
|
|
494
|
+
// "set an alarm" without: "Sure, what size would you like?" (wrong)
|
|
495
|
+
// with: "set an alarm for seven" (better)
|
|
496
|
+
// "play music" without: "" (correct silence)
|
|
497
|
+
// with: "Yes, sweetened or unsweetened?" (wrong)
|
|
498
|
+
//
|
|
499
|
+
// So it displaces some wrong answers and manufactures others, INCLUDING turning
|
|
500
|
+
// a correct silence into a wrong answer — and honest silence is a stated
|
|
501
|
+
// property of this engine (AGENTS §2.13). On the mixed-curriculum store the
|
|
502
|
+
// same shape produced the fragment "nus" for "wake me up at nine am".
|
|
503
|
+
//
|
|
504
|
+
// That evidence is four probes on toy stores and is NOT conclusive; it is,
|
|
505
|
+
// however, the only evidence there is, and it points the wrong way. The stage
|
|
506
|
+
// stays implemented and one env var away. Turn it on (MASSIVE=1) once there is
|
|
507
|
+
// a real measurement showing the recurring fold units it contributes (72.3% of
|
|
508
|
+
// deposited unit mass) buy more than the spurious answers cost.
|
|
509
|
+
const MASSIVE = env("MASSIVE", "0") !== "0";
|
|
510
|
+
const MASSIVE_DATASET = env("MASSIVE_DATASET", "AmazonScience/massive");
|
|
511
|
+
// "all" is the config covering every locale in one set of shards.
|
|
512
|
+
const MASSIVE_CONFIG = env("MASSIVE_CONFIG", "all");
|
|
513
|
+
const MASSIVE_SPLITS = env("MASSIVE_SPLITS", "train")
|
|
514
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
515
|
+
// 0 = every row (587,214 in `all`/train, ~17 MB of content).
|
|
516
|
+
const MASSIVE_MAX_ROWS = Math.max(
|
|
517
|
+
0,
|
|
518
|
+
Math.floor(Number(env("MASSIVE_MAX_ROWS", "0"))) || 0,
|
|
519
|
+
);
|
|
520
|
+
const MAX_MASSIVE_UTT_CHARS = Math.max(
|
|
521
|
+
100,
|
|
522
|
+
Math.floor(Number(env("MAX_MASSIVE_UTT_KB", "2")) * 1000) || 2_000,
|
|
523
|
+
);
|
|
524
|
+
|
|
279
525
|
// ── MuskumPillerum/General-Knowledge (the fourth training stage, after oasst2) ──
|
|
280
526
|
// A ~37.6k-row general-knowledge Q&A set: each row is a single {Question, Answer}
|
|
281
527
|
// pair. A row is a pure RELATION (question → answer), so it becomes exactly ONE
|
|
282
528
|
// FACT, identical in shape to the Aya stage. It ships as a single JSON array
|
|
283
|
-
// file (output.json); we DOWNLOAD it and stream the array.
|
|
284
|
-
// the
|
|
285
|
-
|
|
529
|
+
// file (output.json); we DOWNLOAD it and stream the array. GENKNOW_URL overrides
|
|
530
|
+
// the source.
|
|
531
|
+
//
|
|
532
|
+
// DISABLED BY DEFAULT ON LICENCE GROUNDS (2026-08-13). The HF repo carries NO
|
|
533
|
+
// licence tag and no licence in its card — an earlier header in this file
|
|
534
|
+
// claimed MIT without support — and its own dataset card states it "contains a
|
|
535
|
+
// subset of the alpaca dataset". Alpaca is CC BY-NC 4.0: NonCommercial, which
|
|
536
|
+
// conflicts with Sema's commercial licence. Because a Sema store retains its
|
|
537
|
+
// training text VERBATIM, an unlicensed corpus inside it makes the whole
|
|
538
|
+
// artifact undistributable. See DATASETS.md §3.2. GENKNOW=1 re-enables the
|
|
539
|
+
// stage for local, non-distributed experiments only.
|
|
540
|
+
const GENKNOW = env("GENKNOW", "0") !== "0";
|
|
286
541
|
const GENKNOW_URL = env(
|
|
287
542
|
"GENKNOW_URL",
|
|
288
543
|
"https://huggingface.co/datasets/MuskumPillerum/General-Knowledge/resolve/main/output.json",
|
|
@@ -535,6 +790,13 @@ async function getJson(url: string, label: string): Promise<any> {
|
|
|
535
790
|
|
|
536
791
|
/** A cheap HEAD to learn a download's size (for the cache ceiling and a real
|
|
537
792
|
* ETA). Rate-limits wait; other 4xx is fatal; total failure → 0. */
|
|
793
|
+
/** Advertised transfer size of `url`, used only to reserve cache room. Like any
|
|
794
|
+
* `content-length` this is the ON-THE-WIRE size, so for a content-coded source
|
|
795
|
+
* (GitHub raw gzips JSON ~14x) it UNDER-estimates the file that lands on disk.
|
|
796
|
+
* That is tolerable here because the cache ceiling is a budget, not a
|
|
797
|
+
* correctness property — a run may overshoot MAX_CACHE_GB by the compression
|
|
798
|
+
* ratio of one in-flight file, and each file is deleted as soon as it is
|
|
799
|
+
* consumed. It must NOT be reused as an integrity check; see downloadFile. */
|
|
538
800
|
async function headSize(url: string): Promise<number> {
|
|
539
801
|
return retry(`HEAD ${url}`, async () => {
|
|
540
802
|
const res = await fetch(url, { method: "HEAD", signal: shutdown.signal });
|
|
@@ -606,7 +868,21 @@ async function downloadFile(
|
|
|
606
868
|
if (!res.ok) throw httpError(res);
|
|
607
869
|
if (!res.body) throw new Error("empty response body");
|
|
608
870
|
|
|
609
|
-
|
|
871
|
+
// `content-length` describes the bytes ON THE WIRE. When the server
|
|
872
|
+
// applied a content-coding, fetch hands us the DECODED body, so the
|
|
873
|
+
// header no longer describes what gets written to disk and the integrity
|
|
874
|
+
// guard below must not use it. Measured: raw.githubusercontent.com sends
|
|
875
|
+
// `content-encoding: gzip` with content-length 110,928 for a file that
|
|
876
|
+
// decodes to 1,607,931 bytes — a size check against that rejects every
|
|
877
|
+
// healthy download. (The bug stayed latent because Hugging Face sends
|
|
878
|
+
// `content-encoding: br` and NO content-length, leaving total = 0, which
|
|
879
|
+
// already disables the guard.)
|
|
880
|
+
const encoding = (res.headers.get("content-encoding") ?? "").trim()
|
|
881
|
+
.toLowerCase();
|
|
882
|
+
const decoded = encoding !== "" && encoding !== "identity";
|
|
883
|
+
const total = decoded
|
|
884
|
+
? 0
|
|
885
|
+
: Number(res.headers.get("content-length")) || 0;
|
|
610
886
|
let done = 0;
|
|
611
887
|
|
|
612
888
|
// Stream straight to a ".part" sibling using pure WHATWG streams. A
|
|
@@ -667,9 +943,11 @@ async function downloadFile(
|
|
|
667
943
|
throw e;
|
|
668
944
|
}
|
|
669
945
|
|
|
670
|
-
// Optional integrity guard: when the server advertised a size
|
|
671
|
-
//
|
|
672
|
-
//
|
|
946
|
+
// Optional integrity guard: when the server advertised a size FOR THE
|
|
947
|
+
// BYTES WE WRITE (see the content-encoding note above — `total` is 0 for
|
|
948
|
+
// a decoded body, which disables this), a complete file must match it. A
|
|
949
|
+
// short read (silent truncation) is retried rather than promoted, so the
|
|
950
|
+
// parser never sees a partial file.
|
|
673
951
|
try {
|
|
674
952
|
const got = statSync(partPath).size;
|
|
675
953
|
if (total > 0 && got !== total) {
|
|
@@ -795,15 +1073,18 @@ export function toSmolSentRow(row: unknown): SmolSentRow | null {
|
|
|
795
1073
|
return { src, trg, sl, tl };
|
|
796
1074
|
}
|
|
797
1075
|
|
|
798
|
-
/** Translate ONE SmolSent pair into SEMA facts
|
|
799
|
-
* meaning in two languages,
|
|
800
|
-
*
|
|
1076
|
+
/** Translate ONE SmolSent pair into SEMA facts. The two sentences are one
|
|
1077
|
+
* meaning in two languages, but the two BINDINGS are not equally sound —
|
|
1078
|
+
* SmolSent's English side is a shared pool translated into every language, so
|
|
1079
|
+
* `trg -> src` gives one English context a different answer in every language
|
|
1080
|
+
* file. See SMOLSENT_DIRECTIONS. refineItems drops the degenerate case where
|
|
1081
|
+
* src === trg. */
|
|
801
1082
|
export function smolSentRowToItems(row: SmolSentRow): TrainingItem[] {
|
|
802
1083
|
const { src, trg } = row;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1084
|
+
const items: TrainingItem[] = [];
|
|
1085
|
+
if (SMOLSENT_SRC2TRG) items.push({ context: src, continuation: trg });
|
|
1086
|
+
if (SMOLSENT_TRG2SRC) items.push({ context: trg, continuation: src });
|
|
1087
|
+
return refineItems(items);
|
|
807
1088
|
}
|
|
808
1089
|
|
|
809
1090
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -926,6 +1207,268 @@ export function oasstConversationToItems(turns: OasstTurn[]): TrainingItem[] {
|
|
|
926
1207
|
return refineItems(accumulate(turns.map((t) => t.text)));
|
|
927
1208
|
}
|
|
928
1209
|
|
|
1210
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1211
|
+
// §6e′ Taskmaster 1–4 parsing — a conversation ARRAY ELEMENT → SEMA items
|
|
1212
|
+
//
|
|
1213
|
+
// One adapter serves all four sets: every Taskmaster conversation, in every
|
|
1214
|
+
// set, is `{conversation_id, …, utterances: [{speaker, text, …}]}`.
|
|
1215
|
+
//
|
|
1216
|
+
// ONLY `utterances[].text` IS READ, and that is a licence-adjacent correctness
|
|
1217
|
+
// property, not a stylistic one. TM-3 and TM-4 also carry an `instructions`
|
|
1218
|
+
// field holding the crowd-worker's task template — page after page of
|
|
1219
|
+
// `{{HIDE movie_1 name.movie No Time To Die}}`, `{{CHECK confirm_natural …}}`
|
|
1220
|
+
// and `var_theater_1` placeholders. That is authoring scaffolding, not
|
|
1221
|
+
// dialogue, and depositing it would teach the store template noise as prose.
|
|
1222
|
+
// Reading only `utterances[].text` excludes it structurally. Verified against
|
|
1223
|
+
// the real files: across TM-2 (13,953 turns), TM-3 (24,059) and TM-4 (786),
|
|
1224
|
+
// utterance text contains ZERO `var_*` placeholders and ZERO `{{ }}` markers —
|
|
1225
|
+
// the scaffolding never leaks out of `instructions`.
|
|
1226
|
+
//
|
|
1227
|
+
// CONSECUTIVE SAME-SPEAKER TURNS ARE MERGED. Taskmaster splits one speaker's
|
|
1228
|
+
// contribution across several indexed utterances ("I can help you with your
|
|
1229
|
+
// movie search." / "Where are you located?" are two ASSISTANT rows), which is
|
|
1230
|
+
// an artifact of the collection UI. Left unmerged, the cumulative walk deposits
|
|
1231
|
+
// a turn boundary in the middle of one speaker's contribution and teaches it as
|
|
1232
|
+
// a hand-off. Measured share of turns absorbed by merging: TM-1 17.7%,
|
|
1233
|
+
// TM-2 11.9%, TM-3 0.8%, TM-4 0.0% — so this is load-bearing for the older sets
|
|
1234
|
+
// and a no-op for the newer ones. Speaker names are compared case-insensitively
|
|
1235
|
+
// because TM-1/2 use USER/ASSISTANT and TM-3/4 use user/assistant.
|
|
1236
|
+
//
|
|
1237
|
+
// The deposit shape is the cumulative walk (§6e's `accumulate`), identical to
|
|
1238
|
+
// oasst2: each turn is the continuation of ALL prior turns, bare text, no role
|
|
1239
|
+
// labels. It is the right shape here for the same reason and at a far healthier
|
|
1240
|
+
// size — merged turns run p50 34–45 B (p90 ~100 B) and the accumulated context
|
|
1241
|
+
// p50 301–532 B (p90 ~1.1 KB), against oasst2's median SINGLE turn of 529 B.
|
|
1242
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1243
|
+
|
|
1244
|
+
/** One utterance of a Taskmaster conversation. */
|
|
1245
|
+
export interface TaskmasterTurn {
|
|
1246
|
+
speaker: string; // upper-cased, so TM-1/2 and TM-3/4 compare equal
|
|
1247
|
+
text: string;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** Normalize ONE element of a Taskmaster data file into its turns, or null when
|
|
1251
|
+
* it carries no usable utterance. Empty/whitespace-only utterances are dropped
|
|
1252
|
+
* (TM-3 has a few); a single implausibly long utterance rejects the whole
|
|
1253
|
+
* conversation as corrupt rather than depositing a dump. */
|
|
1254
|
+
export function toTaskmasterTurns(row: unknown): TaskmasterTurn[] | null {
|
|
1255
|
+
if (!row || typeof row !== "object") return null;
|
|
1256
|
+
const utterances = (row as Record<string, unknown>).utterances;
|
|
1257
|
+
if (!Array.isArray(utterances)) return null;
|
|
1258
|
+
const turns: TaskmasterTurn[] = [];
|
|
1259
|
+
for (const u of utterances) {
|
|
1260
|
+
if (!u || typeof u !== "object") continue;
|
|
1261
|
+
const r = u as Record<string, unknown>;
|
|
1262
|
+
const text = typeof r.text === "string" ? r.text.trim() : "";
|
|
1263
|
+
if (!text) continue;
|
|
1264
|
+
if (text.length > MAX_TASKMASTER_TURN_CHARS) return null;
|
|
1265
|
+
turns.push({
|
|
1266
|
+
speaker: String(r.speaker ?? "").trim().toUpperCase(),
|
|
1267
|
+
text,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return turns.length ? turns : null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** Collapse consecutive same-speaker turns into one, joining with a space, and
|
|
1274
|
+
* return the bare texts in order. A turn with no speaker never merges with its
|
|
1275
|
+
* neighbour: an unlabelled row is of unknown origin, and joining two of them
|
|
1276
|
+
* would invent a contribution that may span two speakers. */
|
|
1277
|
+
export function mergeTaskmasterTurns(turns: TaskmasterTurn[]): string[] {
|
|
1278
|
+
const out: string[] = [];
|
|
1279
|
+
let prev = "";
|
|
1280
|
+
for (const t of turns) {
|
|
1281
|
+
if (out.length > 0 && t.speaker !== "" && t.speaker === prev) {
|
|
1282
|
+
out[out.length - 1] += " " + t.text;
|
|
1283
|
+
} else {
|
|
1284
|
+
out.push(t.text);
|
|
1285
|
+
}
|
|
1286
|
+
prev = t.speaker;
|
|
1287
|
+
}
|
|
1288
|
+
return out;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Translate ONE Taskmaster conversation into SEMA training items: the
|
|
1292
|
+
* cumulative walk over its merged turns. Returns [] for a conversation below
|
|
1293
|
+
* TASKMASTER_MIN_TURNS, so callers can simply skip empties. */
|
|
1294
|
+
export function taskmasterConversationToItems(
|
|
1295
|
+
turns: TaskmasterTurn[],
|
|
1296
|
+
): TrainingItem[] {
|
|
1297
|
+
const texts = mergeTaskmasterTurns(turns);
|
|
1298
|
+
if (texts.length < TASKMASTER_MIN_TURNS) return [];
|
|
1299
|
+
return refineItems(accumulate(texts));
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1303
|
+
// §6e″ 2WikiMultihopQA parsing — `evidences` TRIPLES → SEMA facts
|
|
1304
|
+
//
|
|
1305
|
+
// This is the only stage whose purpose is COMPOSITION: answering a question
|
|
1306
|
+
// whose answer no single deposited fact contains. Sema composes by grounding
|
|
1307
|
+
// hop 1, then pivoting on the longest unconsumed learnt context that the
|
|
1308
|
+
// grounded answer CONTAINS (`reason`/`pivotStep`), so the pivot target must
|
|
1309
|
+
// itself be a deposited context. Each triple therefore deposits TWO facts:
|
|
1310
|
+
//
|
|
1311
|
+
// "<subject> <relation>" → "The <relation> of <subject> is <object>."
|
|
1312
|
+
// "<subject>" → "The <relation> of <subject> is <object>."
|
|
1313
|
+
//
|
|
1314
|
+
// The second is the PIVOT FACT. Without it the bare entity naming hop 2's
|
|
1315
|
+
// subject is not a learnt context, so the chain is structurally unreachable no
|
|
1316
|
+
// matter what the rest of the pipeline does.
|
|
1317
|
+
//
|
|
1318
|
+
// MEASURED on 200 real chained dev rows, depositing triples only and asking the
|
|
1319
|
+
// dataset's own composed questions (D = 1024, seed 7):
|
|
1320
|
+
//
|
|
1321
|
+
// relation fact only 240 deposits 5/120 ( 4%) pivotStep 0
|
|
1322
|
+
// relation + pivot fact 800 deposits 44/200 (22%) pivotStep 31
|
|
1323
|
+
//
|
|
1324
|
+
// A 5x improvement, and the only variant where the second hop fires at all.
|
|
1325
|
+
//
|
|
1326
|
+
// REJECTED ALTERNATIVE, so it is not re-tried blind: depositing the pivot fact
|
|
1327
|
+
// only for subjects that also appear as an OBJECT within the same row's
|
|
1328
|
+
// evidences (a row-local "something can pivot into this" test) cut deposits 25%
|
|
1329
|
+
// (800 → 600) but cost composition — 41/200 (20.5%) with pivotStep down to 21,
|
|
1330
|
+
// because real chains also run BETWEEN rows. Composition is this stage's entire
|
|
1331
|
+
// justification, so the deposits are worth keeping.
|
|
1332
|
+
//
|
|
1333
|
+
// The residual ~78% is a KNOWN, previously-recorded limitation and not a defect
|
|
1334
|
+
// in this adapter: the climb elects a topic rather than a relation, so a
|
|
1335
|
+
// question phrased "When did X's father die?" does not align with the Wikidata
|
|
1336
|
+
// property label "date of death". Failure is dominated by hop 2 never firing,
|
|
1337
|
+
// not by a wrong hop 2. Answer-shape breakdown at N = 120: entity answers
|
|
1338
|
+
// 23/106, date answers 2/14 — dates are worse, but not the cliff an earlier
|
|
1339
|
+
// note suggested, which is why no object-shape filter is applied here.
|
|
1340
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1341
|
+
|
|
1342
|
+
/** One (subject, relation, object) triple from a 2Wiki `evidences` cell. */
|
|
1343
|
+
export interface WikiTriple {
|
|
1344
|
+
subject: string;
|
|
1345
|
+
relation: string;
|
|
1346
|
+
object: string;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** Normalize a 2Wiki row into its evidence triples, or null when it carries
|
|
1350
|
+
* none usable. `evidences` is a JSON STRING holding an array of 3-element
|
|
1351
|
+
* arrays; a row whose cell is absent, unparseable, or empty yields null.
|
|
1352
|
+
* Individual malformed or oversized triples are dropped without discarding the
|
|
1353
|
+
* row — one bad triple should not cost the others. */
|
|
1354
|
+
export function toWikiTriples(row: unknown): WikiTriple[] | null {
|
|
1355
|
+
if (!row || typeof row !== "object") return null;
|
|
1356
|
+
const cell = (row as Record<string, unknown>).evidences;
|
|
1357
|
+
let parsed: unknown = cell;
|
|
1358
|
+
if (typeof cell === "string") {
|
|
1359
|
+
try {
|
|
1360
|
+
parsed = JSON.parse(cell);
|
|
1361
|
+
} catch {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (!Array.isArray(parsed)) return null;
|
|
1366
|
+
const out: WikiTriple[] = [];
|
|
1367
|
+
for (const e of parsed) {
|
|
1368
|
+
if (!Array.isArray(e) || e.length < 3) continue;
|
|
1369
|
+
const subject = typeof e[0] === "string" ? e[0].trim() : "";
|
|
1370
|
+
const relation = typeof e[1] === "string" ? e[1].trim() : "";
|
|
1371
|
+
const object = typeof e[2] === "string" ? e[2].trim() : "";
|
|
1372
|
+
if (!subject || !relation || !object) continue;
|
|
1373
|
+
if (
|
|
1374
|
+
subject.length > MAX_WIKI2_FIELD_CHARS ||
|
|
1375
|
+
relation.length > MAX_WIKI2_FIELD_CHARS ||
|
|
1376
|
+
object.length > MAX_WIKI2_FIELD_CHARS
|
|
1377
|
+
) continue;
|
|
1378
|
+
out.push({ subject, relation, object });
|
|
1379
|
+
}
|
|
1380
|
+
return out.length ? out : null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/** Render ONE triple as the prose fact Sema stores. Kept separate so the two
|
|
1384
|
+
* deposits below are guaranteed to share a byte-identical continuation: the
|
|
1385
|
+
* pivot fact only works if it leads to the SAME node the relation fact does. */
|
|
1386
|
+
export function wikiTripleSentence(t: WikiTriple): string {
|
|
1387
|
+
return `The ${t.relation} of ${t.subject} is ${t.object}.`;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** Translate a row's triples into SEMA items: per triple, the relation fact and
|
|
1391
|
+
* the bare-subject PIVOT fact (see the section note above). refineItems drops
|
|
1392
|
+
* the duplicates this produces when a row states the same triple twice. */
|
|
1393
|
+
export function wikiTriplesToItems(triples: WikiTriple[]): TrainingItem[] {
|
|
1394
|
+
const items: TrainingItem[] = [];
|
|
1395
|
+
for (const t of triples) {
|
|
1396
|
+
const fact = wikiTripleSentence(t);
|
|
1397
|
+
items.push({ context: `${t.subject} ${t.relation}`, continuation: fact });
|
|
1398
|
+
items.push({ context: t.subject, continuation: fact });
|
|
1399
|
+
}
|
|
1400
|
+
return refineItems(items);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1404
|
+
// §6e‴ SODA parsing — a social dialogue row → SEMA items
|
|
1405
|
+
//
|
|
1406
|
+
// Each row carries `dialogue` (an array of turn strings) and `speakers` (the
|
|
1407
|
+
// speaker name per turn). The deposit is the cumulative walk over speaker-merged
|
|
1408
|
+
// turns, identical in shape to Taskmaster and oasst2 — turns are short (mean
|
|
1409
|
+
// 87 B) and dialogues average 7.3 turns, so the accumulated context stays well
|
|
1410
|
+
// inside the healthy range.
|
|
1411
|
+
//
|
|
1412
|
+
// `narrative`, `literal` and the ATOMIC-style `head`/`relation`/`tail` columns
|
|
1413
|
+
// are NOT deposited: they are the generation scaffolding SODA was distilled
|
|
1414
|
+
// from, they restate the dialogue in the third person, and depositing both a
|
|
1415
|
+
// dialogue and its paraphrased summary gives one meaning two shapes — which is
|
|
1416
|
+
// measured to SUPPRESS composition rather than help it.
|
|
1417
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1418
|
+
|
|
1419
|
+
/** Normalize a SODA row into its turns, or null when it carries no usable
|
|
1420
|
+
* dialogue. Speakers are optional (they only drive merging); an implausibly
|
|
1421
|
+
* long turn rejects the dialogue as corrupt. */
|
|
1422
|
+
export function toSodaTurns(row: unknown): TaskmasterTurn[] | null {
|
|
1423
|
+
if (!row || typeof row !== "object") return null;
|
|
1424
|
+
const r = row as Record<string, unknown>;
|
|
1425
|
+
const dialogue = r.dialogue;
|
|
1426
|
+
if (!Array.isArray(dialogue)) return null;
|
|
1427
|
+
const speakers = Array.isArray(r.speakers) ? r.speakers : [];
|
|
1428
|
+
const turns: TaskmasterTurn[] = [];
|
|
1429
|
+
for (let i = 0; i < dialogue.length; i++) {
|
|
1430
|
+
const text = typeof dialogue[i] === "string"
|
|
1431
|
+
? (dialogue[i] as string).trim()
|
|
1432
|
+
: "";
|
|
1433
|
+
if (!text) continue;
|
|
1434
|
+
if (text.length > MAX_SODA_TURN_CHARS) return null;
|
|
1435
|
+
turns.push({
|
|
1436
|
+
speaker: String(speakers[i] ?? "").trim().toUpperCase(),
|
|
1437
|
+
text,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
return turns.length ? turns : null;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** Translate ONE SODA dialogue into SEMA items: the cumulative walk over its
|
|
1444
|
+
* speaker-merged turns. Shares `mergeTaskmasterTurns` because the rule is the
|
|
1445
|
+
* same one — consecutive turns by one speaker are one contribution. */
|
|
1446
|
+
export function sodaDialogueToItems(turns: TaskmasterTurn[]): TrainingItem[] {
|
|
1447
|
+
const texts = mergeTaskmasterTurns(turns);
|
|
1448
|
+
if (texts.length < 2) return []; // not an exchange
|
|
1449
|
+
return refineItems(accumulate(texts));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1453
|
+
// §6e⁗ MASSIVE parsing — one short utterance → ONE SEMA experience
|
|
1454
|
+
//
|
|
1455
|
+
// See the constants note for why this deposits a bare experience and not a
|
|
1456
|
+
// relation: the two relational shapes this corpus appears to offer are both
|
|
1457
|
+
// false (same-intent rows are not paraphrases; same-id rows across locales are
|
|
1458
|
+
// translations, SmolSent's worst-scoring relation).
|
|
1459
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1460
|
+
|
|
1461
|
+
/** Translate ONE MASSIVE row into SEMA items: its bare utterance, as an
|
|
1462
|
+
* experience. `annot_utt` (slot-annotated) is deliberately not used — its
|
|
1463
|
+
* "[date : ...]" markup is not prose. Returns [] for an unusable row. */
|
|
1464
|
+
export function massiveRowToItems(row: unknown): TrainingItem[] {
|
|
1465
|
+
if (!row || typeof row !== "object") return [];
|
|
1466
|
+
const utt = (row as Record<string, unknown>).utt;
|
|
1467
|
+
const text = typeof utt === "string" ? utt.trim() : "";
|
|
1468
|
+
if (!text || text.length > MAX_MASSIVE_UTT_CHARS) return [];
|
|
1469
|
+
return refineItems([text]);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
929
1472
|
// ═══════════════════════════════════════════════════════════════════════
|
|
930
1473
|
// §6f General-Knowledge parsing — a {Question, Answer} row → SEMA fact
|
|
931
1474
|
//
|
|
@@ -1142,6 +1685,77 @@ async function listSmolSentFiles(): Promise<string[]> {
|
|
|
1142
1685
|
return paths.filter((p) => want.has(basename(p).replace(/\.jsonl$/i, "")));
|
|
1143
1686
|
}
|
|
1144
1687
|
|
|
1688
|
+
/** List the Taskmaster data files to train, in TASKMASTER_SETS order. Returns
|
|
1689
|
+
* repo-relative paths, e.g. "TM-3-2020/data/data_00.json".
|
|
1690
|
+
*
|
|
1691
|
+
* TM-2/3/4 keep their dialogue files under `<set>/data`, so everything there is
|
|
1692
|
+
* fair game. TM-1 has no `data` directory: its two dialogue files sit at the
|
|
1693
|
+
* set root NEXT TO `ontology.json` (a slot schema) and `sample.json` (a small
|
|
1694
|
+
* excerpt of self-dialogs). Neither is an array of conversations, and training
|
|
1695
|
+
* the excerpt would deposit a subset of TM-1 twice, so TM-1 is filtered to the
|
|
1696
|
+
* `*-dialogs.json` pair (self-dialogs, woz-dialogs). */
|
|
1697
|
+
async function listTaskmasterFiles(): Promise<
|
|
1698
|
+
Array<{ set: string; path: string }>
|
|
1699
|
+
> {
|
|
1700
|
+
const out: Array<{ set: string; path: string }> = [];
|
|
1701
|
+
for (const set of TASKMASTER_SETS) {
|
|
1702
|
+
const rootOnly = /^TM-1\b/i.test(set);
|
|
1703
|
+
const dir = rootOnly ? set : `${set}/data`;
|
|
1704
|
+
const body = await getJson(
|
|
1705
|
+
`https://api.github.com/repos/${TASKMASTER_REPO}/contents/${dir}`,
|
|
1706
|
+
`GET Taskmaster ${dir}`,
|
|
1707
|
+
);
|
|
1708
|
+
const names: string[] = Array.isArray(body)
|
|
1709
|
+
? body
|
|
1710
|
+
.filter((e: any) => e?.type === "file" && /\.json$/i.test(e?.name))
|
|
1711
|
+
.map((e: any) => String(e.name))
|
|
1712
|
+
: [];
|
|
1713
|
+
names.sort();
|
|
1714
|
+
for (const name of names) {
|
|
1715
|
+
if (rootOnly && !/-dialogs\.json$/i.test(name)) continue;
|
|
1716
|
+
out.push({ set, path: `${dir}/${name}` });
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return out;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/** List a dataset's Parquet shards on Hugging Face's auto-converted
|
|
1723
|
+
* `refs/convert/parquet` branch, restricted to `config` and to `splits`.
|
|
1724
|
+
*
|
|
1725
|
+
* Shared by 2Wiki, SODA and MASSIVE. The converted branch is used rather than
|
|
1726
|
+
* `main` because a dataset's own Parquet may be written as ONE giant row-group
|
|
1727
|
+
* (SODA's is 1,191,582 rows), and a column chunk is per-group, so reading any
|
|
1728
|
+
* part of it materialises all of it. The converted branch is uniformly
|
|
1729
|
+
* 10,000-row groups. See test/79-parquet-batching.test.mjs.
|
|
1730
|
+
*
|
|
1731
|
+
* Paths look like "<config>/<split>/0000.parquet". The BRANCH name is a single
|
|
1732
|
+
* path SEGMENT here, so its "/" is percent-encoded — unlike a dataset id,
|
|
1733
|
+
* whose "/" must not be. */
|
|
1734
|
+
async function listConvertedParquet(
|
|
1735
|
+
dataset: string,
|
|
1736
|
+
config: string,
|
|
1737
|
+
splits: string[],
|
|
1738
|
+
label: string,
|
|
1739
|
+
): Promise<string[]> {
|
|
1740
|
+
const body = await getJson(
|
|
1741
|
+
`https://huggingface.co/api/datasets/${dataset}` +
|
|
1742
|
+
`/tree/refs%2Fconvert%2Fparquet/${config}?recursive=true`,
|
|
1743
|
+
`GET ${label} tree`,
|
|
1744
|
+
);
|
|
1745
|
+
const paths: string[] = Array.isArray(body)
|
|
1746
|
+
? body
|
|
1747
|
+
.filter((e: any) => e?.type === "file" && /\.parquet$/i.test(e?.path))
|
|
1748
|
+
.map((e: any) => String(e.path))
|
|
1749
|
+
: [];
|
|
1750
|
+
paths.sort();
|
|
1751
|
+
const want = new Set(splits);
|
|
1752
|
+
return paths.filter((p) => {
|
|
1753
|
+
const parts = p.split("/");
|
|
1754
|
+
// The tree is rooted at `config`, so the split is the second-to-last part.
|
|
1755
|
+
return want.has(parts[parts.length - 2] ?? "");
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1145
1759
|
/** Stream a plain-JSONL file from disk, deposit each parsed row via `toItems`.
|
|
1146
1760
|
* Lines are split without buffering the whole file; an oversize/malformed line
|
|
1147
1761
|
* is counted skipped and the stream continues. Shared by SmolSent (and any
|
|
@@ -1227,16 +1841,59 @@ async function processJsonl(
|
|
|
1227
1841
|
}
|
|
1228
1842
|
}
|
|
1229
1843
|
|
|
1230
|
-
/**
|
|
1844
|
+
/** How many rows to materialise in one read from a row-group of `rgRows` rows
|
|
1845
|
+
* occupying `groupBytes` uncompressed bytes, under a `budgetBytes` target.
|
|
1846
|
+
*
|
|
1847
|
+
* The group's own footer statistics give the mean row width, so the batch
|
|
1848
|
+
* follows the CORPUS's row size rather than the writer's layout: wide rows
|
|
1849
|
+
* (SODA carries a whole dialogue per row) batch smaller than narrow ones at
|
|
1850
|
+
* the same memory cost. Never exceeds the group — a batch is a subdivision of
|
|
1851
|
+
* a group, never a span across two, because `parquetReadObjects` is given an
|
|
1852
|
+
* absolute row range and column chunks are per-group. Never returns 0, or the
|
|
1853
|
+
* read loop could not advance.
|
|
1854
|
+
*
|
|
1855
|
+
* A writer that omits `total_byte_size` yields `groupBytes <= 0`; the batch is
|
|
1856
|
+
* then the whole group, which is exactly the behaviour this replaced. That
|
|
1857
|
+
* fallback is safe for every file we read today (all three report it) and
|
|
1858
|
+
* degrades to the old memory profile rather than to a wrong result. */
|
|
1859
|
+
export function parquetBatchRows(
|
|
1860
|
+
rgRows: number,
|
|
1861
|
+
groupBytes: number,
|
|
1862
|
+
budgetBytes: number,
|
|
1863
|
+
): number {
|
|
1864
|
+
if (!(rgRows > 0)) return 0; // empty group — the caller skips it
|
|
1865
|
+
if (!(groupBytes > 0) || !Number.isFinite(groupBytes)) return rgRows;
|
|
1866
|
+
const perRow = groupBytes / rgRows;
|
|
1867
|
+
const fit = Math.floor(budgetBytes / perRow);
|
|
1868
|
+
return Math.min(rgRows, Math.max(1, fit));
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/** Read a downloaded Parquet file in bounded row batches with hyparquet (+Snappy
|
|
1231
1872
|
* from hyparquet-compressors) over a web-standard Blob byte source, depositing
|
|
1232
|
-
* each row via `toItems`.
|
|
1233
|
-
* multi-hundred-MB file
|
|
1873
|
+
* each row via `toItems`. At most `PARQUET_BATCH_BYTES` of source rows are
|
|
1874
|
+
* materialised at a time, so neither a multi-hundred-MB file nor a file written
|
|
1875
|
+
* as ONE giant row-group loads whole into memory.
|
|
1876
|
+
*
|
|
1877
|
+
* Batching also makes a single-group file INTERRUPTIBLE: the abort check runs
|
|
1878
|
+
* per batch, where before a 1.19M-row group could not be cancelled at all. */
|
|
1234
1879
|
async function processParquet(
|
|
1235
1880
|
filePath: string,
|
|
1236
1881
|
toItems: (row: unknown) => TrainingItem[] | null,
|
|
1237
1882
|
ci: CachedIngest,
|
|
1238
1883
|
onExample: (contentBytes: number) => Promise<boolean>,
|
|
1239
1884
|
sample: (it: TrainingItem) => void,
|
|
1885
|
+
// Optional stage-level stop, checked per row and before each batch is
|
|
1886
|
+
// decoded. A stage BUDGET must stop the read rather than reject rows: left to
|
|
1887
|
+
// reject, a budgeted stage still DECODES every remaining row-group — 143,346
|
|
1888
|
+
// rows of one 86.7 MB SODA shard — and reports them as "unusable" when
|
|
1889
|
+
// nothing was wrong with them, which is a lie in the run log.
|
|
1890
|
+
//
|
|
1891
|
+
// Measured honestly: on that shard the wall time did NOT improve (2m 35s ->
|
|
1892
|
+
// 2m 37s), because a budgeted run is dominated by depositing the rows it DID
|
|
1893
|
+
// take, not by scanning past the ones it did not. The win here is a truthful
|
|
1894
|
+
// log and the CPU/allocation of ~143k skipped row decodes, not elapsed time.
|
|
1895
|
+
// A larger shard past a small budget is where the decode cost would show.
|
|
1896
|
+
shouldStop?: () => boolean,
|
|
1240
1897
|
): Promise<FileResult> {
|
|
1241
1898
|
const blob = await openAsBlob(filePath);
|
|
1242
1899
|
const file = {
|
|
@@ -1248,28 +1905,39 @@ async function processParquet(
|
|
|
1248
1905
|
let examples = 0, skipped = 0;
|
|
1249
1906
|
let rowStart = 0;
|
|
1250
1907
|
for (const rg of meta.row_groups) {
|
|
1251
|
-
if (shutdown.signal.aborted) return { examples, stopped: true, skipped };
|
|
1252
1908
|
const rgRows = Number(rg.num_rows);
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
rowStart
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1909
|
+
const rgEnd = rowStart + rgRows;
|
|
1910
|
+
const batchRows = parquetBatchRows(
|
|
1911
|
+
rgRows,
|
|
1912
|
+
Number(rg.total_byte_size ?? 0),
|
|
1913
|
+
PARQUET_BATCH_BYTES,
|
|
1914
|
+
);
|
|
1915
|
+
if (batchRows <= 0) continue; // empty group
|
|
1916
|
+
// Materialise one bounded batch at a time, then deposit its rows.
|
|
1917
|
+
while (rowStart < rgEnd) {
|
|
1918
|
+
if (shutdown.signal.aborted) return { examples, stopped: true, skipped };
|
|
1919
|
+
if (shouldStop?.()) return { examples, stopped: true, skipped };
|
|
1920
|
+
const rowEnd = Math.min(rowStart + batchRows, rgEnd);
|
|
1921
|
+
const rows = await parquetReadObjects({
|
|
1922
|
+
file,
|
|
1923
|
+
compressors,
|
|
1924
|
+
rowStart,
|
|
1925
|
+
rowEnd,
|
|
1926
|
+
});
|
|
1927
|
+
rowStart = rowEnd;
|
|
1928
|
+
for (const row of rows) {
|
|
1929
|
+
if (shouldStop?.()) return { examples, stopped: true, skipped };
|
|
1930
|
+
const items = toItems(row);
|
|
1931
|
+
if (!items || items.length === 0) {
|
|
1932
|
+
skipped++;
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1936
|
+
examples++;
|
|
1937
|
+
return onExample(contentBytes);
|
|
1938
|
+
}, sample);
|
|
1939
|
+
if (!ok) return { examples, stopped: true, skipped };
|
|
1267
1940
|
}
|
|
1268
|
-
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1269
|
-
examples++;
|
|
1270
|
-
return onExample(contentBytes);
|
|
1271
|
-
}, sample);
|
|
1272
|
-
if (!ok) return { examples, stopped: true, skipped };
|
|
1273
1941
|
}
|
|
1274
1942
|
}
|
|
1275
1943
|
return { examples, stopped: false, skipped };
|
|
@@ -1683,7 +2351,10 @@ async function main(): Promise<void> {
|
|
|
1683
2351
|
process.exit(1);
|
|
1684
2352
|
}
|
|
1685
2353
|
|
|
1686
|
-
await store.setMeta(
|
|
2354
|
+
await store.setMeta(
|
|
2355
|
+
"train.dataset",
|
|
2356
|
+
"SmolSent+Aya+oasst2+Taskmaster+2Wiki+SODA+MASSIVE",
|
|
2357
|
+
);
|
|
1687
2358
|
await store.setMeta("train.D", String(D));
|
|
1688
2359
|
await store.setMeta("train.seed", String(SEED));
|
|
1689
2360
|
await store.setMeta("train.createdAt", new Date().toISOString());
|
|
@@ -2581,6 +3252,478 @@ async function main(): Promise<void> {
|
|
|
2581
3252
|
const r = toGenKnowRow(row);
|
|
2582
3253
|
return r ? genKnowRowToItems(r) : null;
|
|
2583
3254
|
};
|
|
3255
|
+
// ── §10d′ Taskmaster 1–4 stage (task-oriented dialogue; runs AFTER oasst2) ──
|
|
3256
|
+
//
|
|
3257
|
+
// Lists the repo's data files, then for each: download, parse the JSON array,
|
|
3258
|
+
// deposit one cumulative walk per conversation. Per-FILE resume ids, like
|
|
3259
|
+
// SmolSent — an interrupted file is re-read from the top on resume, and
|
|
3260
|
+
// re-deposition is idempotent. LOCAL_PATH/taskmaster/ may hold pre-downloaded
|
|
3261
|
+
// *.json (a subdirectory, because these share the .json extension with the
|
|
3262
|
+
// General-Knowledge source and must not be confused with it).
|
|
3263
|
+
const tmToItems = (row: unknown): TrainingItem[] | null => {
|
|
3264
|
+
const turns = toTaskmasterTurns(row);
|
|
3265
|
+
if (!turns) return null;
|
|
3266
|
+
const items = taskmasterConversationToItems(turns); // [] when too short
|
|
3267
|
+
return items.length ? items : null;
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3270
|
+
const trainTaskmaster = async (): Promise<void> => {
|
|
3271
|
+
if (!TASKMASTER) return;
|
|
3272
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) return;
|
|
3273
|
+
|
|
3274
|
+
let files: Array<
|
|
3275
|
+
{ id: string; name: string; local?: string; url?: string }
|
|
3276
|
+
>;
|
|
3277
|
+
if (LOCAL_PATH) {
|
|
3278
|
+
const dir = join(LOCAL_PATH, "taskmaster");
|
|
3279
|
+
let names: string[] = [];
|
|
3280
|
+
try {
|
|
3281
|
+
names = readdirSync(dir).filter((f: string) => /\.json$/i.test(f))
|
|
3282
|
+
.sort();
|
|
3283
|
+
} catch { /* no local taskmaster dir */ }
|
|
3284
|
+
if (names.length === 0) {
|
|
3285
|
+
progress.log(
|
|
3286
|
+
` ${DIM}· no Taskmaster *.json in ${dir} — skipping${R}`,
|
|
3287
|
+
);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
files = names.map((f: string) => ({
|
|
3291
|
+
id: `taskmaster::${f}`,
|
|
3292
|
+
name: f,
|
|
3293
|
+
local: join(dir, f),
|
|
3294
|
+
}));
|
|
3295
|
+
} else {
|
|
3296
|
+
let listed: Array<{ set: string; path: string }>;
|
|
3297
|
+
try {
|
|
3298
|
+
listed = await listTaskmasterFiles();
|
|
3299
|
+
} catch (e) {
|
|
3300
|
+
if (stopRequested || (e as Error)?.name === "AbortError") return;
|
|
3301
|
+
progress.log(
|
|
3302
|
+
` ${RED}✗${R} Taskmaster file listing failed: ${
|
|
3303
|
+
(e as Error).message
|
|
3304
|
+
}`,
|
|
3305
|
+
);
|
|
3306
|
+
return;
|
|
3307
|
+
}
|
|
3308
|
+
files = listed.map((f) => ({
|
|
3309
|
+
id: `taskmaster::${f.path}`,
|
|
3310
|
+
name: `${f.set}/${basename(f.path)}`,
|
|
3311
|
+
url: `${TASKMASTER_RAW}/${f.path}`,
|
|
3312
|
+
}));
|
|
3313
|
+
}
|
|
3314
|
+
if (files.length === 0) {
|
|
3315
|
+
progress.log(` ${DIM}· no Taskmaster files found — skipping${R}`);
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
const p = await loadProgress(store);
|
|
3320
|
+
const done = new Set(p.completedFiles);
|
|
3321
|
+
const remaining = files.filter((f) => !done.has(f.id));
|
|
3322
|
+
if (remaining.length === 0) {
|
|
3323
|
+
progress.log(` ${DIM}· Taskmaster already trained — skipping${R}`);
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
state.fileTotal = files.length;
|
|
3327
|
+
progress.log(
|
|
3328
|
+
` ${GRN}✓${R} Taskmaster: ${remaining.length}/${files.length} dialogue file(s) to train`,
|
|
3329
|
+
);
|
|
3330
|
+
|
|
3331
|
+
let idx = 0;
|
|
3332
|
+
for (const f of files) {
|
|
3333
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) break;
|
|
3334
|
+
idx++;
|
|
3335
|
+
if (done.has(f.id)) continue;
|
|
3336
|
+
|
|
3337
|
+
let path = f.local ?? "";
|
|
3338
|
+
let downloaded = false;
|
|
3339
|
+
if (!path) {
|
|
3340
|
+
const got = await acquire(
|
|
3341
|
+
f.url!,
|
|
3342
|
+
f.id.replace(/[^A-Za-z0-9._-]+/g, "_"),
|
|
3343
|
+
`Taskmaster ${f.name}`,
|
|
3344
|
+
);
|
|
3345
|
+
if (!got) {
|
|
3346
|
+
if (stopRequested) break;
|
|
3347
|
+
continue; // a single failed file never aborts the stage
|
|
3348
|
+
}
|
|
3349
|
+
path = got;
|
|
3350
|
+
downloaded = true;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
try {
|
|
3354
|
+
totalCorpusBytes += statSync(path).size;
|
|
3355
|
+
} catch { /* best effort */ }
|
|
3356
|
+
|
|
3357
|
+
state.activity = "process";
|
|
3358
|
+
state.fileIndex = idx;
|
|
3359
|
+
state.filePath = `Taskmaster ${f.name}`;
|
|
3360
|
+
state.fileExamples = 0;
|
|
3361
|
+
tick(true);
|
|
3362
|
+
const p0 = Date.now();
|
|
3363
|
+
let res: FileResult;
|
|
3364
|
+
try {
|
|
3365
|
+
res = await processJsonArray(path, tmToItems, ci, onDeposit, sample);
|
|
3366
|
+
} catch (e) {
|
|
3367
|
+
if (stopRequested || (e as Error)?.name === "AbortError") break;
|
|
3368
|
+
progress.log(
|
|
3369
|
+
` ${RED}✗${R} Taskmaster ${f.name} parse failed: ${
|
|
3370
|
+
(e as Error).message
|
|
3371
|
+
}`,
|
|
3372
|
+
);
|
|
3373
|
+
if (downloaded) {
|
|
3374
|
+
try {
|
|
3375
|
+
unlinkSync(path);
|
|
3376
|
+
} catch { /* best effort */ }
|
|
3377
|
+
}
|
|
3378
|
+
continue;
|
|
3379
|
+
}
|
|
3380
|
+
langTally["taskmaster"] = (langTally["taskmaster"] ?? 0) + res.examples;
|
|
3381
|
+
progress.log(
|
|
3382
|
+
` ${GRN}✓${R} ${f.name} ${DIM}[task dialogue]${R} → ${
|
|
3383
|
+
int(res.examples)
|
|
3384
|
+
} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
3385
|
+
(res.skipped
|
|
3386
|
+
? ` ${YEL}· ${
|
|
3387
|
+
int(res.skipped)
|
|
3388
|
+
} unusable conversation(s) skipped${R}`
|
|
3389
|
+
: "") +
|
|
3390
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
3391
|
+
);
|
|
3392
|
+
|
|
3393
|
+
if (!res.stopped) {
|
|
3394
|
+
try {
|
|
3395
|
+
totalBytesProcessed += statSync(path).size;
|
|
3396
|
+
} catch { /* best effort */ }
|
|
3397
|
+
if (downloaded) {
|
|
3398
|
+
try {
|
|
3399
|
+
unlinkSync(path);
|
|
3400
|
+
} catch { /* best effort */ }
|
|
3401
|
+
}
|
|
3402
|
+
done.add(f.id);
|
|
3403
|
+
p.completedFiles.push(f.id);
|
|
3404
|
+
}
|
|
3405
|
+
try {
|
|
3406
|
+
await saveProgress(store, {
|
|
3407
|
+
completedFiles: p.completedFiles,
|
|
3408
|
+
depositCount,
|
|
3409
|
+
trainedContentBytes,
|
|
3410
|
+
totalBytesProcessed,
|
|
3411
|
+
totalCorpusBytes,
|
|
3412
|
+
});
|
|
3413
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
3414
|
+
} catch { /* best effort — finish() will retry */ }
|
|
3415
|
+
if (res.stopped) break; // cap/signal — leave file un-completed for resume
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
|
|
3419
|
+
// ── §10d″ 2WikiMultihopQA stage (composition; runs AFTER Taskmaster) ──
|
|
3420
|
+
//
|
|
3421
|
+
// Only the `evidences` column is ever touched — see §6e″ for why `context`
|
|
3422
|
+
// and `question`/`answer` are not.
|
|
3423
|
+
const wiki2ToItems = (row: unknown): TrainingItem[] | null => {
|
|
3424
|
+
const triples = toWikiTriples(row);
|
|
3425
|
+
if (!triples) return null;
|
|
3426
|
+
const items = wikiTriplesToItems(triples);
|
|
3427
|
+
return items.length ? items : null;
|
|
3428
|
+
};
|
|
3429
|
+
|
|
3430
|
+
const trainWiki2 = (): Promise<void> =>
|
|
3431
|
+
runConvertedParquetStage({
|
|
3432
|
+
enabled: WIKI2,
|
|
3433
|
+
label: "2Wiki",
|
|
3434
|
+
tally: "2wiki",
|
|
3435
|
+
kind: "relation triples",
|
|
3436
|
+
dataset: WIKI2_DATASET,
|
|
3437
|
+
config: "default",
|
|
3438
|
+
splits: WIKI2_SPLITS,
|
|
3439
|
+
localDir: "2wiki",
|
|
3440
|
+
maxRows: WIKI2_MAX_ROWS,
|
|
3441
|
+
toItems: wiki2ToItems,
|
|
3442
|
+
});
|
|
3443
|
+
|
|
3444
|
+
// ── §10d‴ Converted-Parquet stage runner (2Wiki, SODA, MASSIVE) ──
|
|
3445
|
+
//
|
|
3446
|
+
// These three differ only in their name, their row adapter and their row
|
|
3447
|
+
// budget, so they share one runner instead of three copies of the per-shard
|
|
3448
|
+
// loop. Resume is per SHARD, as everywhere else.
|
|
3449
|
+
//
|
|
3450
|
+
// The BUDGET is applied here rather than inside an adapter because it is a
|
|
3451
|
+
// curriculum decision about corpus MIX, not a property of a row: SODA's train
|
|
3452
|
+
// split would otherwise contribute ~8M episodes against the 662,221 deposits
|
|
3453
|
+
// of the whole current corpus. A budgeted stage never marks its remaining
|
|
3454
|
+
// shards complete, so raising the budget later resumes rather than restarts.
|
|
3455
|
+
const runConvertedParquetStage = async (opts: {
|
|
3456
|
+
enabled: boolean;
|
|
3457
|
+
label: string; // human name, e.g. "SODA"
|
|
3458
|
+
tally: string; // langTally key
|
|
3459
|
+
kind: string; // dim tag in the log line, e.g. "social dialogue"
|
|
3460
|
+
dataset: string;
|
|
3461
|
+
config: string;
|
|
3462
|
+
splits: string[];
|
|
3463
|
+
localDir: string; // subdirectory of LOCAL_PATH
|
|
3464
|
+
maxRows: number; // 0 = no budget
|
|
3465
|
+
toItems: (row: unknown) => TrainingItem[] | null;
|
|
3466
|
+
}): Promise<void> => {
|
|
3467
|
+
if (!opts.enabled) return;
|
|
3468
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) return;
|
|
3469
|
+
|
|
3470
|
+
let files: Array<
|
|
3471
|
+
{ id: string; name: string; local?: string; url?: string }
|
|
3472
|
+
>;
|
|
3473
|
+
if (LOCAL_PATH) {
|
|
3474
|
+
const dir = join(LOCAL_PATH, opts.localDir);
|
|
3475
|
+
let names: string[] = [];
|
|
3476
|
+
try {
|
|
3477
|
+
names = readdirSync(dir).filter((f: string) => /\.parquet$/i.test(f))
|
|
3478
|
+
.sort();
|
|
3479
|
+
} catch { /* no local dir for this stage */ }
|
|
3480
|
+
if (names.length === 0) {
|
|
3481
|
+
progress.log(
|
|
3482
|
+
` ${DIM}· no ${opts.label} *.parquet in ${dir} — skipping${R}`,
|
|
3483
|
+
);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
files = names.map((f: string) => ({
|
|
3487
|
+
id: `${opts.tally}::${f}`,
|
|
3488
|
+
name: f,
|
|
3489
|
+
local: join(dir, f),
|
|
3490
|
+
}));
|
|
3491
|
+
} else {
|
|
3492
|
+
let paths: string[];
|
|
3493
|
+
try {
|
|
3494
|
+
paths = await listConvertedParquet(
|
|
3495
|
+
opts.dataset,
|
|
3496
|
+
opts.config,
|
|
3497
|
+
opts.splits,
|
|
3498
|
+
opts.label,
|
|
3499
|
+
);
|
|
3500
|
+
} catch (e) {
|
|
3501
|
+
if (stopRequested || (e as Error)?.name === "AbortError") return;
|
|
3502
|
+
progress.log(
|
|
3503
|
+
` ${RED}✗${R} ${opts.label} file listing failed: ${
|
|
3504
|
+
(e as Error).message
|
|
3505
|
+
}`,
|
|
3506
|
+
);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
files = paths.map((path) => ({
|
|
3510
|
+
id: `${opts.tally}::${path}`,
|
|
3511
|
+
name: path,
|
|
3512
|
+
url: `https://huggingface.co/datasets/${opts.dataset}` +
|
|
3513
|
+
`/resolve/refs%2Fconvert%2Fparquet/${path}`,
|
|
3514
|
+
}));
|
|
3515
|
+
}
|
|
3516
|
+
if (files.length === 0) {
|
|
3517
|
+
progress.log(` ${DIM}· no ${opts.label} files found — skipping${R}`);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
const p = await loadProgress(store);
|
|
3522
|
+
const done = new Set(p.completedFiles);
|
|
3523
|
+
// A budget-limited stage never marks its later shards complete — that is
|
|
3524
|
+
// what lets a raised budget resume instead of restarting. But it also means
|
|
3525
|
+
// "every shard complete" is NOT how such a stage finishes, so without a
|
|
3526
|
+
// marker of its own a satisfied budget would re-read and re-deposit its
|
|
3527
|
+
// rows on every subsequent run: harmless to the store (deposition is
|
|
3528
|
+
// idempotent) but it repeats the work and double-counts langTally.
|
|
3529
|
+
//
|
|
3530
|
+
// The marker carries the budget it was satisfied AT, so raising the budget
|
|
3531
|
+
// still resumes: a bigger budget does not match the marker and the stage
|
|
3532
|
+
// runs again, re-reading rows it already holds (idempotent) and adding the
|
|
3533
|
+
// new ones.
|
|
3534
|
+
const budgetMark = opts.maxRows > 0
|
|
3535
|
+
? `${opts.tally}::budget=${opts.maxRows}`
|
|
3536
|
+
: "";
|
|
3537
|
+
if (budgetMark && done.has(budgetMark)) {
|
|
3538
|
+
progress.log(
|
|
3539
|
+
` ${DIM}· ${opts.label} budget of ${
|
|
3540
|
+
int(opts.maxRows)
|
|
3541
|
+
} row(s) already met — skipping${R}`,
|
|
3542
|
+
);
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
const remaining = files.filter((f) => !done.has(f.id));
|
|
3546
|
+
if (remaining.length === 0) {
|
|
3547
|
+
progress.log(` ${DIM}· ${opts.label} already trained — skipping${R}`);
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
state.fileTotal = files.length;
|
|
3551
|
+
progress.log(
|
|
3552
|
+
` ${GRN}✓${R} ${opts.label}: ${remaining.length}/${files.length} shard(s) to train` +
|
|
3553
|
+
(opts.maxRows > 0
|
|
3554
|
+
? ` ${DIM}(budget ${int(opts.maxRows)} rows)${R}`
|
|
3555
|
+
: ""),
|
|
3556
|
+
);
|
|
3557
|
+
|
|
3558
|
+
// The budget spans the whole stage, not one shard, so it is counted here.
|
|
3559
|
+
let rowsTaken = 0;
|
|
3560
|
+
const spent = () => opts.maxRows > 0 && rowsTaken >= opts.maxRows;
|
|
3561
|
+
const budgeted = (row: unknown): TrainingItem[] | null => {
|
|
3562
|
+
const items = opts.toItems(row);
|
|
3563
|
+
if (!items || items.length === 0) return null;
|
|
3564
|
+
rowsTaken++;
|
|
3565
|
+
return items;
|
|
3566
|
+
};
|
|
3567
|
+
|
|
3568
|
+
let idx = 0;
|
|
3569
|
+
for (const f of files) {
|
|
3570
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) break;
|
|
3571
|
+
if (opts.maxRows > 0 && rowsTaken >= opts.maxRows) break;
|
|
3572
|
+
idx++;
|
|
3573
|
+
if (done.has(f.id)) continue;
|
|
3574
|
+
|
|
3575
|
+
let path = f.local ?? "";
|
|
3576
|
+
let downloaded = false;
|
|
3577
|
+
if (!path) {
|
|
3578
|
+
const got = await acquire(
|
|
3579
|
+
f.url!,
|
|
3580
|
+
f.id.replace(/[^A-Za-z0-9._-]+/g, "_"),
|
|
3581
|
+
`${opts.label} ${f.name}`,
|
|
3582
|
+
);
|
|
3583
|
+
if (!got) {
|
|
3584
|
+
if (stopRequested) break;
|
|
3585
|
+
continue; // a single failed shard never aborts the stage
|
|
3586
|
+
}
|
|
3587
|
+
path = got;
|
|
3588
|
+
downloaded = true;
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
try {
|
|
3592
|
+
totalCorpusBytes += statSync(path).size;
|
|
3593
|
+
} catch { /* best effort */ }
|
|
3594
|
+
|
|
3595
|
+
state.activity = "process";
|
|
3596
|
+
state.fileIndex = idx;
|
|
3597
|
+
state.filePath = `${opts.label} ${f.name}`;
|
|
3598
|
+
state.fileExamples = 0;
|
|
3599
|
+
tick(true);
|
|
3600
|
+
const p0 = Date.now();
|
|
3601
|
+
const before = rowsTaken;
|
|
3602
|
+
let res: FileResult;
|
|
3603
|
+
try {
|
|
3604
|
+
res = await processParquet(
|
|
3605
|
+
path,
|
|
3606
|
+
budgeted,
|
|
3607
|
+
ci,
|
|
3608
|
+
onDeposit,
|
|
3609
|
+
sample,
|
|
3610
|
+
spent,
|
|
3611
|
+
);
|
|
3612
|
+
} catch (e) {
|
|
3613
|
+
if (stopRequested || (e as Error)?.name === "AbortError") break;
|
|
3614
|
+
progress.log(
|
|
3615
|
+
` ${RED}✗${R} ${opts.label} ${f.name} parse failed: ${
|
|
3616
|
+
(e as Error).message
|
|
3617
|
+
}`,
|
|
3618
|
+
);
|
|
3619
|
+
if (downloaded) {
|
|
3620
|
+
try {
|
|
3621
|
+
unlinkSync(path);
|
|
3622
|
+
} catch { /* best effort */ }
|
|
3623
|
+
}
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
// A shard cut short by the BUDGET is not "done" — leave it resumable so a
|
|
3627
|
+
// later run with a bigger budget continues instead of starting over.
|
|
3628
|
+
const hitBudget = spent();
|
|
3629
|
+
langTally[opts.tally] = (langTally[opts.tally] ?? 0) + res.examples;
|
|
3630
|
+
progress.log(
|
|
3631
|
+
` ${GRN}✓${R} ${f.name} ${DIM}[${opts.kind}]${R} → ${
|
|
3632
|
+
int(res.examples)
|
|
3633
|
+
} facts ${DIM}from ${int(rowsTaken - before)} row(s) in ${
|
|
3634
|
+
dur((Date.now() - p0) / 1000)
|
|
3635
|
+
}${R}` +
|
|
3636
|
+
(res.skipped
|
|
3637
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
3638
|
+
: "") +
|
|
3639
|
+
(hitBudget
|
|
3640
|
+
? ` ${YEL}(budget reached)${R}`
|
|
3641
|
+
: res.stopped
|
|
3642
|
+
? ` ${YEL}(stopped early)${R}`
|
|
3643
|
+
: ""),
|
|
3644
|
+
);
|
|
3645
|
+
|
|
3646
|
+
if (!res.stopped && !hitBudget) {
|
|
3647
|
+
try {
|
|
3648
|
+
totalBytesProcessed += statSync(path).size;
|
|
3649
|
+
} catch { /* best effort */ }
|
|
3650
|
+
if (downloaded) {
|
|
3651
|
+
try {
|
|
3652
|
+
unlinkSync(path);
|
|
3653
|
+
} catch { /* best effort */ }
|
|
3654
|
+
}
|
|
3655
|
+
done.add(f.id);
|
|
3656
|
+
p.completedFiles.push(f.id);
|
|
3657
|
+
}
|
|
3658
|
+
try {
|
|
3659
|
+
await saveProgress(store, {
|
|
3660
|
+
completedFiles: p.completedFiles,
|
|
3661
|
+
depositCount,
|
|
3662
|
+
trainedContentBytes,
|
|
3663
|
+
totalBytesProcessed,
|
|
3664
|
+
totalCorpusBytes,
|
|
3665
|
+
});
|
|
3666
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
3667
|
+
} catch { /* best effort — finish() will retry */ }
|
|
3668
|
+
// A budget stop is not a cap/signal stop: the stage is finished, so fall
|
|
3669
|
+
// out of the loop rather than treating it as an interruption.
|
|
3670
|
+
if (res.stopped && !hitBudget) break;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// Record a satisfied budget so the next run skips this stage instead of
|
|
3674
|
+
// re-reading it. Only when the budget was actually reached: a stage that
|
|
3675
|
+
// ran out of shards first is complete by the normal per-shard rule, and a
|
|
3676
|
+
// stage cut short by MAX_MB or Ctrl+C must stay resumable.
|
|
3677
|
+
if (budgetMark && spent() && !stopRequested && !done.has(budgetMark)) {
|
|
3678
|
+
p.completedFiles.push(budgetMark);
|
|
3679
|
+
try {
|
|
3680
|
+
await saveProgress(store, {
|
|
3681
|
+
completedFiles: p.completedFiles,
|
|
3682
|
+
depositCount,
|
|
3683
|
+
trainedContentBytes,
|
|
3684
|
+
totalBytesProcessed,
|
|
3685
|
+
totalCorpusBytes,
|
|
3686
|
+
});
|
|
3687
|
+
} catch { /* best effort — finish() will retry */ }
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
|
|
3691
|
+
const trainSoda = (): Promise<void> =>
|
|
3692
|
+
runConvertedParquetStage({
|
|
3693
|
+
enabled: SODA,
|
|
3694
|
+
label: "SODA",
|
|
3695
|
+
tally: "soda",
|
|
3696
|
+
kind: "social dialogue",
|
|
3697
|
+
dataset: SODA_DATASET,
|
|
3698
|
+
config: "default",
|
|
3699
|
+
splits: SODA_SPLITS,
|
|
3700
|
+
localDir: "soda",
|
|
3701
|
+
maxRows: SODA_MAX_DIALOGS,
|
|
3702
|
+
toItems: (row) => {
|
|
3703
|
+
const turns = toSodaTurns(row);
|
|
3704
|
+
if (!turns) return null;
|
|
3705
|
+
const items = sodaDialogueToItems(turns);
|
|
3706
|
+
return items.length ? items : null;
|
|
3707
|
+
},
|
|
3708
|
+
});
|
|
3709
|
+
|
|
3710
|
+
const trainMassive = (): Promise<void> =>
|
|
3711
|
+
runConvertedParquetStage({
|
|
3712
|
+
enabled: MASSIVE,
|
|
3713
|
+
label: "MASSIVE",
|
|
3714
|
+
tally: "massive",
|
|
3715
|
+
kind: "short intents",
|
|
3716
|
+
dataset: MASSIVE_DATASET,
|
|
3717
|
+
config: MASSIVE_CONFIG,
|
|
3718
|
+
splits: MASSIVE_SPLITS,
|
|
3719
|
+
localDir: "massive",
|
|
3720
|
+
maxRows: MASSIVE_MAX_ROWS,
|
|
3721
|
+
toItems: (row) => {
|
|
3722
|
+
const items = massiveRowToItems(row);
|
|
3723
|
+
return items.length ? items : null;
|
|
3724
|
+
},
|
|
3725
|
+
});
|
|
3726
|
+
|
|
2584
3727
|
const trainGenKnow = async (): Promise<void> => {
|
|
2585
3728
|
if (!GENKNOW) return;
|
|
2586
3729
|
if (trainedContentBytes >= MAX_BYTES || stopRequested) return;
|
|
@@ -2714,6 +3857,10 @@ async function main(): Promise<void> {
|
|
|
2714
3857
|
if (!stopRequested) await trainSmolSent();
|
|
2715
3858
|
if (!stopRequested) await trainAya();
|
|
2716
3859
|
if (!stopRequested) await trainOasst();
|
|
3860
|
+
if (!stopRequested) await trainTaskmaster();
|
|
3861
|
+
if (!stopRequested) await trainWiki2();
|
|
3862
|
+
if (!stopRequested) await trainSoda();
|
|
3863
|
+
if (!stopRequested) await trainMassive();
|
|
2717
3864
|
if (!stopRequested) await trainGenKnow();
|
|
2718
3865
|
|
|
2719
3866
|
await finish(stopRequested ? stopReason : "done");
|