@hviana/sema 0.1.4 → 0.1.5
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/dist/example/demo.js +25 -25
- package/dist/example/train_base.d.ts +21 -19
- package/dist/example/train_base.js +1952 -1684
- package/dist/src/alphabet.d.ts +3 -3
- package/dist/src/alphabet.js +27 -24
- package/dist/src/alu/src/alu.d.ts +194 -169
- package/dist/src/alu/src/alu.js +414 -374
- package/dist/src/alu/src/expr.d.ts +43 -36
- package/dist/src/alu/src/expr.js +278 -252
- package/dist/src/alu/src/index.d.ts +98 -9
- package/dist/src/alu/src/index.js +63 -9
- package/dist/src/alu/src/kernel-arith.d.ts +5 -1
- package/dist/src/alu/src/kernel-arith.js +285 -195
- package/dist/src/alu/src/kernel-bits.d.ts +5 -1
- package/dist/src/alu/src/kernel-bits.js +115 -81
- package/dist/src/alu/src/kernel-logic.js +63 -33
- package/dist/src/alu/src/kernel-nd.js +195 -155
- package/dist/src/alu/src/kernel-numeric.d.ts +80 -16
- package/dist/src/alu/src/kernel-numeric.js +331 -259
- package/dist/src/alu/src/operation.d.ts +132 -112
- package/dist/src/alu/src/operation.js +163 -149
- package/dist/src/alu/src/parser.d.ts +175 -168
- package/dist/src/alu/src/parser.js +421 -382
- package/dist/src/alu/src/resonance.d.ts +25 -16
- package/dist/src/alu/src/resonance.js +56 -49
- package/dist/src/alu/src/text.d.ts +7 -3
- package/dist/src/alu/src/text.js +37 -31
- package/dist/src/alu/src/value.d.ts +14 -14
- package/dist/src/alu/src/value.js +160 -150
- package/dist/src/alu/test/alu.test.js +656 -543
- package/dist/src/bytes.d.ts +5 -1
- package/dist/src/bytes.js +40 -33
- package/dist/src/config.d.ts +102 -102
- package/dist/src/config.js +83 -83
- package/dist/src/derive/src/deduction.d.ts +62 -60
- package/dist/src/derive/src/deduction.js +109 -103
- package/dist/src/derive/src/index.d.ts +7 -1
- package/dist/src/derive/src/priority-queue.d.ts +8 -8
- package/dist/src/derive/src/priority-queue.js +61 -57
- package/dist/src/derive/src/rewrite.d.ts +18 -15
- package/dist/src/derive/src/rewrite.js +67 -59
- package/dist/src/derive/src/trie.d.ts +56 -56
- package/dist/src/derive/src/trie.js +179 -175
- package/dist/src/derive/test/derive.test.js +100 -96
- package/dist/src/extension.d.ts +16 -13
- package/dist/src/geometry.d.ts +42 -17
- package/dist/src/geometry.js +266 -235
- package/dist/src/index.d.ts +17 -2
- package/dist/src/index.js +7 -1
- package/dist/src/ingest-cache.d.ts +30 -25
- package/dist/src/ingest-cache.js +127 -108
- package/dist/src/mind/articulation.d.ts +5 -1
- package/dist/src/mind/articulation.js +112 -77
- package/dist/src/mind/attention.d.ts +108 -40
- package/dist/src/mind/attention.js +871 -749
- package/dist/src/mind/canonical.d.ts +19 -4
- package/dist/src/mind/canonical.js +31 -27
- package/dist/src/mind/graph-search.d.ts +225 -201
- package/dist/src/mind/graph-search.js +821 -742
- package/dist/src/mind/index.d.ts +10 -2
- package/dist/src/mind/junction.d.ts +58 -31
- package/dist/src/mind/junction.js +237 -172
- package/dist/src/mind/learning.d.ts +52 -16
- package/dist/src/mind/learning.js +165 -143
- package/dist/src/mind/match.d.ts +69 -20
- package/dist/src/mind/match.js +318 -259
- package/dist/src/mind/mechanisms/alu.js +16 -16
- package/dist/src/mind/mechanisms/cast.d.ts +13 -9
- package/dist/src/mind/mechanisms/cast.js +456 -363
- package/dist/src/mind/mechanisms/confluence.d.ts +12 -8
- package/dist/src/mind/mechanisms/confluence.js +183 -152
- package/dist/src/mind/mechanisms/cover.d.ts +8 -2
- package/dist/src/mind/mechanisms/cover.js +210 -148
- package/dist/src/mind/mechanisms/extraction.d.ts +34 -8
- package/dist/src/mind/mechanisms/extraction.js +288 -234
- package/dist/src/mind/mechanisms/recall.d.ts +10 -6
- package/dist/src/mind/mechanisms/recall.js +185 -126
- package/dist/src/mind/mind.d.ts +154 -129
- package/dist/src/mind/mind.js +288 -258
- package/dist/src/mind/pipeline-mechanism.d.ts +124 -112
- package/dist/src/mind/pipeline-mechanism.js +172 -161
- package/dist/src/mind/pipeline.d.ts +14 -4
- package/dist/src/mind/pipeline.js +189 -125
- package/dist/src/mind/primitives.d.ts +32 -8
- package/dist/src/mind/primitives.js +117 -99
- package/dist/src/mind/rationale.d.ts +98 -86
- package/dist/src/mind/rationale.js +121 -113
- package/dist/src/mind/reasoning.d.ts +13 -2
- package/dist/src/mind/reasoning.js +166 -129
- package/dist/src/mind/recognition.d.ts +4 -1
- package/dist/src/mind/recognition.js +208 -183
- package/dist/src/mind/resonance.d.ts +22 -5
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +25 -6
- package/dist/src/mind/trace.js +58 -50
- package/dist/src/mind/traverse.d.ts +58 -16
- package/dist/src/mind/traverse.js +357 -304
- package/dist/src/mind/types.d.ts +127 -120
- package/dist/src/mind/types.js +69 -60
- package/dist/src/rabitq-hnsw/src/database.d.ts +179 -177
- package/dist/src/rabitq-hnsw/src/database.js +359 -342
- package/dist/src/rabitq-hnsw/src/heap.d.ts +12 -12
- package/dist/src/rabitq-hnsw/src/heap.js +80 -75
- package/dist/src/rabitq-hnsw/src/hnsw.d.ts +103 -103
- package/dist/src/rabitq-hnsw/src/hnsw.js +454 -428
- package/dist/src/rabitq-hnsw/src/index.d.ts +6 -1
- package/dist/src/rabitq-hnsw/src/prng.d.ts +10 -10
- package/dist/src/rabitq-hnsw/src/prng.js +27 -25
- package/dist/src/rabitq-hnsw/src/rabitq.d.ts +69 -69
- package/dist/src/rabitq-hnsw/src/rabitq.js +262 -246
- package/dist/src/rabitq-hnsw/src/store.d.ts +132 -132
- package/dist/src/rabitq-hnsw/src/store.js +843 -752
- package/dist/src/rabitq-hnsw/test/hnsw.test.js +1054 -805
- package/dist/src/sema.d.ts +13 -9
- package/dist/src/sema.js +40 -26
- package/dist/src/store-sqlite.d.ts +166 -144
- package/dist/src/store-sqlite.js +660 -578
- package/dist/src/store.d.ts +630 -593
- package/dist/src/store.js +1581 -1440
- package/dist/src/vec.d.ts +9 -5
- package/dist/src/vec.js +73 -61
- package/package.json +2 -2
|
@@ -101,7 +101,19 @@ import { CachedIngest, Mind, SQliteStore } from "../src/index.js";
|
|
|
101
101
|
// writing a file is the single capability the web platform does not expose, so
|
|
102
102
|
// the download sink uses the synchronous fs descriptor calls below. The durable
|
|
103
103
|
// disk cache is therefore the sole, irreducible Node dependency.
|
|
104
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
closeSync,
|
|
106
|
+
existsSync,
|
|
107
|
+
fsyncSync,
|
|
108
|
+
mkdirSync,
|
|
109
|
+
openAsBlob,
|
|
110
|
+
openSync,
|
|
111
|
+
readdirSync,
|
|
112
|
+
renameSync,
|
|
113
|
+
statSync,
|
|
114
|
+
unlinkSync,
|
|
115
|
+
writeSync,
|
|
116
|
+
} from "node:fs";
|
|
105
117
|
import { basename, join } from "node:path";
|
|
106
118
|
// The ONLY third-party dependencies, and only for the one source that ships
|
|
107
119
|
// exclusively as Snappy-compressed Parquet (Aya): hyparquet is a pure-JS,
|
|
@@ -131,13 +143,16 @@ const env = (k, d) => process.env[k] ?? d;
|
|
|
131
143
|
const SMOLSENT = env("SMOLSENT", "1") !== "0";
|
|
132
144
|
const SMOLSENT_DATASET = env("SMOLSENT_DATASET", "google/smol");
|
|
133
145
|
const SMOLSENT_PAIRS = (process.env.SMOLSENT_PAIRS ?? "")
|
|
134
|
-
|
|
146
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
135
147
|
// The resume id PREFIX for the SmolSent stage; one completed-files entry per
|
|
136
148
|
// file (e.g. "smolsent::ha_en.jsonl").
|
|
137
149
|
const SMOLSENT_ID = "smolsent";
|
|
138
150
|
// A SmolSent side longer than this is skipped (a sentence pair is short; a huge
|
|
139
151
|
// value is corruption, not a sentence).
|
|
140
|
-
const MAX_SMOLSENT_CHARS = Math.max(
|
|
152
|
+
const MAX_SMOLSENT_CHARS = Math.max(
|
|
153
|
+
2_000,
|
|
154
|
+
Math.floor(Number(env("MAX_SMOLSENT_KB", "16")) * 1000) || 16_000,
|
|
155
|
+
);
|
|
141
156
|
const DB_PATH = env("DB_PATH", "sema"); // → {DB_PATH}.sqlite
|
|
142
157
|
const D = Number(env("D", "1024"));
|
|
143
158
|
const SEED = Number(env("SEED", "7"));
|
|
@@ -147,7 +162,10 @@ const SEED = Number(env("SEED", "7"));
|
|
|
147
162
|
// deposit checkpoint, nor silently disable checkpointing. The tail (a run that
|
|
148
163
|
// learns less than one interval, or the remainder past the last interval) is
|
|
149
164
|
// always saved by finish() at exit — a complete point.
|
|
150
|
-
const CHECKPOINT_BYTES = Math.max(
|
|
165
|
+
const CHECKPOINT_BYTES = Math.max(
|
|
166
|
+
1_000_000,
|
|
167
|
+
Math.floor(Number(env("CHECKPOINT_MB", "100")) * 1_000_000) || 100_000_000,
|
|
168
|
+
);
|
|
151
169
|
const LOCAL_PATH = env("LOCAL_PATH", ""); // train from a local dir of *.zip
|
|
152
170
|
const CACHE_DIR = env("CACHE_DIR", join(process.cwd(), "cache"));
|
|
153
171
|
const MAX_CACHE_BYTES = Number(env("MAX_CACHE_GB", "100")) * 1e9;
|
|
@@ -182,8 +200,10 @@ let lastThrottleLog = 0;
|
|
|
182
200
|
// carries the running total to/past the ceiling (that item is still counted).
|
|
183
201
|
const MAX_MB = Number(env("MAX_MB", "Infinity"));
|
|
184
202
|
if (isNaN(MAX_MB) || MAX_MB < 0) {
|
|
185
|
-
|
|
186
|
-
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
`fatal: MAX_MB must be a non-negative number or "Infinity"\n`,
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
187
207
|
}
|
|
188
208
|
const MAX_BYTES = MAX_MB * 1_000_000; // Infinity stays Infinity
|
|
189
209
|
// ── CohereLabs/aya_dataset (the second training stage, after SmolSent) ──
|
|
@@ -195,14 +215,20 @@ const MAX_BYTES = MAX_MB * 1_000_000; // Infinity stays Infinity
|
|
|
195
215
|
// web-standard Blob byte source — no whole-file-in-memory load. AYA=0 disables
|
|
196
216
|
// the stage; AYA_URL overrides the Parquet source.
|
|
197
217
|
const AYA = env("AYA", "1") !== "0";
|
|
198
|
-
const AYA_URL = env(
|
|
218
|
+
const AYA_URL = env(
|
|
219
|
+
"AYA_URL",
|
|
220
|
+
"https://huggingface.co/datasets/CohereLabs/aya_dataset/resolve/main/data/train-00000-of-00001.parquet",
|
|
221
|
+
);
|
|
199
222
|
// The resume id of the Aya stage, kept in the same completed-files set as the
|
|
200
223
|
// other stages, so one store records the whole curriculum.
|
|
201
224
|
const AYA_ID = "aya::dataset";
|
|
202
225
|
// A single Aya field this many chars or longer is skipped: inputs/targets range
|
|
203
226
|
// up to ~3.3M chars, and a multi-MB "pair" is documentation/dump noise, not a
|
|
204
227
|
// cognitive example.
|
|
205
|
-
const MAX_AYA_FIELD_CHARS = Math.max(
|
|
228
|
+
const MAX_AYA_FIELD_CHARS = Math.max(
|
|
229
|
+
10_000,
|
|
230
|
+
Math.floor(Number(env("MAX_AYA_FIELD_KB", "256")) * 1000) || 256_000,
|
|
231
|
+
);
|
|
206
232
|
// ── OpenAssistant/oasst2 (the fourth training stage, after Aya) ──
|
|
207
233
|
// oasst2 is a corpus of human↔assistant conversation TREES. Its richest, most
|
|
208
234
|
// stream-friendly artifact is "<date>_oasst2_ready.trees.jsonl.gz": one JSON
|
|
@@ -215,16 +241,25 @@ const MAX_AYA_FIELD_CHARS = Math.max(10_000, Math.floor(Number(env("MAX_AYA_FIEL
|
|
|
215
241
|
// least two full user→assistant exchanges) — single Q→A trees are skipped, by
|
|
216
242
|
// design. OASST=0 disables the stage; OASST_URL overrides the source.
|
|
217
243
|
const OASST = env("OASST", "1") !== "0";
|
|
218
|
-
const OASST_URL = env(
|
|
244
|
+
const OASST_URL = env(
|
|
245
|
+
"OASST_URL",
|
|
246
|
+
"https://huggingface.co/datasets/OpenAssistant/oasst2/resolve/main/2023-11-05_oasst2_ready.trees.jsonl.gz",
|
|
247
|
+
);
|
|
219
248
|
// The resume id of the oasst2 stage, in the same completed-files set as the
|
|
220
249
|
// other stages, so one store records the whole curriculum.
|
|
221
250
|
const OASST_ID = "oasst2::trees";
|
|
222
251
|
// Multi-turn threshold: a conversation must have at least this many turns to be
|
|
223
252
|
// trained (4 = user→assistant→user→assistant, the smallest real multi-turn).
|
|
224
|
-
const OASST_MIN_TURNS = Math.max(
|
|
253
|
+
const OASST_MIN_TURNS = Math.max(
|
|
254
|
+
2,
|
|
255
|
+
Math.floor(Number(env("OASST_MIN_TURNS", "4"))) || 4,
|
|
256
|
+
);
|
|
225
257
|
// Skip a tree whose decoded JSON line exceeds this (a pathological record); the
|
|
226
258
|
// real maximum is far smaller, so this only guards against corruption.
|
|
227
|
-
const MAX_OASST_LINE_CHARS = Math.max(
|
|
259
|
+
const MAX_OASST_LINE_CHARS = Math.max(
|
|
260
|
+
100_000,
|
|
261
|
+
Math.floor(Number(env("MAX_OASST_LINE_MB", "8")) * 1_000_000) || 8_000_000,
|
|
262
|
+
);
|
|
228
263
|
// ── MuskumPillerum/General-Knowledge (the fourth training stage, after oasst2) ──
|
|
229
264
|
// A ~37.6k-row general-knowledge Q&A set: each row is a single {Question, Answer}
|
|
230
265
|
// pair. A row is a pure RELATION (question → answer), so it becomes exactly ONE
|
|
@@ -232,13 +267,19 @@ const MAX_OASST_LINE_CHARS = Math.max(100_000, Math.floor(Number(env("MAX_OASST_
|
|
|
232
267
|
// file (output.json); we DOWNLOAD it and stream the array. GENKNOW=0 disables
|
|
233
268
|
// the stage; GENKNOW_URL overrides the source.
|
|
234
269
|
const GENKNOW = env("GENKNOW", "1") !== "0";
|
|
235
|
-
const GENKNOW_URL = env(
|
|
270
|
+
const GENKNOW_URL = env(
|
|
271
|
+
"GENKNOW_URL",
|
|
272
|
+
"https://huggingface.co/datasets/MuskumPillerum/General-Knowledge/resolve/main/output.json",
|
|
273
|
+
);
|
|
236
274
|
// The resume id of the General-Knowledge stage, in the same completed-files set
|
|
237
275
|
// as the other stages, so one store records the whole curriculum.
|
|
238
276
|
const GENKNOW_ID = "genknow::qa";
|
|
239
277
|
// A Question/Answer longer than this is skipped (answers run to a few hundred
|
|
240
278
|
// chars; this only guards against a corrupt/runaway field).
|
|
241
|
-
const MAX_GENKNOW_CHARS = Math.max(
|
|
279
|
+
const MAX_GENKNOW_CHARS = Math.max(
|
|
280
|
+
4_000,
|
|
281
|
+
Math.floor(Number(env("MAX_GENKNOW_KB", "64")) * 1000) || 64_000,
|
|
282
|
+
);
|
|
242
283
|
// ═══════════════════════════════════════════════════════════════════════
|
|
243
284
|
// §2 Terminal + formatting helpers
|
|
244
285
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -249,91 +290,105 @@ const YEL = `${CSI}33m`, RED = `${CSI}31m`;
|
|
|
249
290
|
const HIDE = `${CSI}?25l`, SHOW = `${CSI}?25h`;
|
|
250
291
|
/** Sleep `ms`, but wake early if the shutdown signal fires — so a long back-off
|
|
251
292
|
* (e.g. a rate-limit wait) never swallows Ctrl+C. Resolves either way. */
|
|
252
|
-
const waitMs = (ms) =>
|
|
253
|
-
|
|
254
|
-
|
|
293
|
+
const waitMs = (ms) =>
|
|
294
|
+
new Promise((resolve) => {
|
|
295
|
+
if (shutdown.signal.aborted) {
|
|
296
|
+
return resolve();
|
|
297
|
+
}
|
|
255
298
|
// NOTE: the timer is deliberately NOT unref'd — an unref'd timer does not
|
|
256
299
|
// keep the event loop alive, so a pending wait (e.g. the pace between page
|
|
257
300
|
// requests, or a rate-limit back-off) would let Node exit early and the run
|
|
258
301
|
// would "do nothing and close". The listener lets a shutdown wake it early.
|
|
259
302
|
const t = setTimeout(done, ms);
|
|
260
303
|
function done() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
304
|
+
clearTimeout(t);
|
|
305
|
+
shutdown.signal.removeEventListener("abort", done);
|
|
306
|
+
resolve();
|
|
264
307
|
}
|
|
265
308
|
shutdown.signal.addEventListener("abort", done, { once: true });
|
|
266
|
-
});
|
|
309
|
+
});
|
|
267
310
|
/** Resolve `p`, but reject with a TimeoutError if it takes longer than `ms`.
|
|
268
311
|
* The underlying promise is left to settle on its own (we just stop waiting),
|
|
269
312
|
* so a slow black-box call can never wedge the caller. */
|
|
270
313
|
function withTimeout(p, ms, label = "operation") {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const t = setTimeout(() => {
|
|
316
|
+
const e = new Error(`${label} timed out after ${ms}ms`);
|
|
317
|
+
e.name = "TimeoutError";
|
|
318
|
+
reject(e);
|
|
319
|
+
}, ms);
|
|
320
|
+
if (typeof t.unref === "function") {
|
|
321
|
+
t.unref();
|
|
322
|
+
}
|
|
323
|
+
p.then((v) => {
|
|
324
|
+
clearTimeout(t);
|
|
325
|
+
resolve(v);
|
|
326
|
+
}, (e) => {
|
|
327
|
+
clearTimeout(t);
|
|
328
|
+
reject(e);
|
|
286
329
|
});
|
|
330
|
+
});
|
|
287
331
|
}
|
|
288
332
|
/** Human-readable duration from seconds. */
|
|
289
333
|
function dur(seconds) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
334
|
+
if (!isFinite(seconds) || seconds < 0) {
|
|
335
|
+
return "--";
|
|
336
|
+
}
|
|
337
|
+
const h = Math.floor(seconds / 3600);
|
|
338
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
339
|
+
const s = Math.floor(seconds % 60);
|
|
340
|
+
if (h > 0) {
|
|
341
|
+
return `${h}h ${m}m ${s}s`;
|
|
342
|
+
}
|
|
343
|
+
if (m > 0) {
|
|
344
|
+
return `${m}m ${s}s`;
|
|
345
|
+
}
|
|
346
|
+
return `${s}s`;
|
|
300
347
|
}
|
|
301
348
|
/** Human-readable byte size. */
|
|
302
349
|
function bytes(n) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
350
|
+
if (!isFinite(n) || n < 0) {
|
|
351
|
+
return "--";
|
|
352
|
+
}
|
|
353
|
+
if (n < 1024) {
|
|
354
|
+
return `${n} B`;
|
|
355
|
+
}
|
|
356
|
+
if (n < 1e6) {
|
|
357
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
358
|
+
}
|
|
359
|
+
if (n < 1e9) {
|
|
360
|
+
return `${(n / 1e6).toFixed(1)} MB`;
|
|
361
|
+
}
|
|
362
|
+
return `${(n / 1e9).toFixed(2)} GB`;
|
|
312
363
|
}
|
|
313
364
|
/** Short count: 1234567 → "1.23M". */
|
|
314
365
|
function num(n) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
366
|
+
if (n >= 1e9) {
|
|
367
|
+
return `${(n / 1e9).toFixed(2)}B`;
|
|
368
|
+
}
|
|
369
|
+
if (n >= 1e6) {
|
|
370
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
371
|
+
}
|
|
372
|
+
if (n >= 1e3) {
|
|
373
|
+
return `${(n / 1e3).toFixed(1)}K`;
|
|
374
|
+
}
|
|
375
|
+
return String(n);
|
|
322
376
|
}
|
|
323
377
|
const int = (n) => Math.round(n).toLocaleString("en-US");
|
|
324
378
|
const clamp01 = (f) => Math.max(0, Math.min(1, f));
|
|
325
379
|
const pct = (f) => `${(clamp01(f) * 100).toFixed(1)}%`;
|
|
326
380
|
/** A progress bar of width `w` filled to fraction `frac`. */
|
|
327
381
|
function bar(w, frac) {
|
|
328
|
-
|
|
329
|
-
|
|
382
|
+
const filled = Math.round(clamp01(frac) * w);
|
|
383
|
+
return `${GRN}${"█".repeat(filled)}${GREY}${"░".repeat(w - filled)}${R}`;
|
|
330
384
|
}
|
|
331
385
|
/** Collapse whitespace and clip to `max` chars with an ellipsis. */
|
|
332
386
|
function clip(text, max) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
387
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
388
|
+
if (max < 1) {
|
|
389
|
+
return "";
|
|
390
|
+
}
|
|
391
|
+
return t.length <= max ? t : t.slice(0, max - 1) + "…";
|
|
337
392
|
}
|
|
338
393
|
/** Retry `fn` with exponential backoff.
|
|
339
394
|
*
|
|
@@ -350,46 +405,46 @@ function clip(text, max) {
|
|
|
350
405
|
* `onFail` is called after each non-throttle failed attempt; `onThrottle` after
|
|
351
406
|
* each throttle wait (for a "waiting…" notice). */
|
|
352
407
|
async function retry(label, fn, tries, onFail, onThrottle) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
try {
|
|
361
|
-
return await fn();
|
|
362
|
-
}
|
|
363
|
-
catch (e) {
|
|
364
|
-
const err = e;
|
|
365
|
-
if (err.name === "AbortError" || err.fatal)
|
|
366
|
-
throw err;
|
|
367
|
-
// Rate-limited / overloaded: wait it out. Does NOT advance `attempt`, so a
|
|
368
|
-
// busy server can never exhaust the retry budget and drop the request.
|
|
369
|
-
if (err.throttle && !shutdown.signal.aborted) {
|
|
370
|
-
throttleHits++;
|
|
371
|
-
// Honour Retry-After when the server sent one; else exponential back-off
|
|
372
|
-
// with jitter, capped, so a fleet of requests does not resynchronise.
|
|
373
|
-
const base = err.retryAfterMs && err.retryAfterMs > 0
|
|
374
|
-
? err.retryAfterMs
|
|
375
|
-
: throttleWait;
|
|
376
|
-
const ms = Math.min(base, 60_000) +
|
|
377
|
-
Math.floor(base * 0.25 * Math.random());
|
|
378
|
-
onThrottle?.(ms);
|
|
379
|
-
await waitMs(ms);
|
|
380
|
-
throttleWait = Math.min(throttleWait * 2, 60_000);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
last = err.message;
|
|
384
|
-
onFail?.(attempt, err);
|
|
385
|
-
attempt++;
|
|
386
|
-
if (attempt <= tries) {
|
|
387
|
-
await waitMs(wait);
|
|
388
|
-
wait = Math.min(wait * 2, 30_000);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
408
|
+
let wait = 1000, last = "", throttleWait = 1000, throttleHits = 0;
|
|
409
|
+
for (let attempt = 1; attempt <= tries;) {
|
|
410
|
+
if (shutdown.signal.aborted) {
|
|
411
|
+
const e = new Error("aborted");
|
|
412
|
+
e.fatal = true;
|
|
413
|
+
throw e;
|
|
391
414
|
}
|
|
392
|
-
|
|
415
|
+
try {
|
|
416
|
+
return await fn();
|
|
417
|
+
} catch (e) {
|
|
418
|
+
const err = e;
|
|
419
|
+
if (err.name === "AbortError" || err.fatal) {
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
// Rate-limited / overloaded: wait it out. Does NOT advance `attempt`, so a
|
|
423
|
+
// busy server can never exhaust the retry budget and drop the request.
|
|
424
|
+
if (err.throttle && !shutdown.signal.aborted) {
|
|
425
|
+
throttleHits++;
|
|
426
|
+
// Honour Retry-After when the server sent one; else exponential back-off
|
|
427
|
+
// with jitter, capped, so a fleet of requests does not resynchronise.
|
|
428
|
+
const base = err.retryAfterMs && err.retryAfterMs > 0
|
|
429
|
+
? err.retryAfterMs
|
|
430
|
+
: throttleWait;
|
|
431
|
+
const ms = Math.min(base, 60_000) +
|
|
432
|
+
Math.floor(base * 0.25 * Math.random());
|
|
433
|
+
onThrottle?.(ms);
|
|
434
|
+
await waitMs(ms);
|
|
435
|
+
throttleWait = Math.min(throttleWait * 2, 60_000);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
last = err.message;
|
|
439
|
+
onFail?.(attempt, err);
|
|
440
|
+
attempt++;
|
|
441
|
+
if (attempt <= tries) {
|
|
442
|
+
await waitMs(wait);
|
|
443
|
+
wait = Math.min(wait * 2, 30_000);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
throw new Error(`${label} failed after ${tries} attempts: ${last}`);
|
|
393
448
|
}
|
|
394
449
|
/** Classify a non-OK HTTP response into an {@link HttpError} for {@link retry}:
|
|
395
450
|
* • 429 / 503 → THROTTLE (rate-limited / overloaded): retried indefinitely,
|
|
@@ -398,26 +453,25 @@ async function retry(label, fn, tries, onFail, onThrottle) {
|
|
|
398
453
|
* • other 4xx → FATAL: a real client error (404, 401, …) — not retried.
|
|
399
454
|
* Never throttles forever silently: the wait is interruptible by shutdown. */
|
|
400
455
|
function httpError(res) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
456
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
457
|
+
if (res.status === 429 || res.status === 503) {
|
|
458
|
+
err.throttle = true;
|
|
459
|
+
const ra = res.headers.get("retry-after");
|
|
460
|
+
if (ra) {
|
|
461
|
+
const secs = Number(ra);
|
|
462
|
+
if (Number.isFinite(secs)) {
|
|
463
|
+
err.retryAfterMs = Math.max(0, secs * 1000);
|
|
464
|
+
} else {
|
|
465
|
+
const when = Date.parse(ra);
|
|
466
|
+
if (Number.isFinite(when)) {
|
|
467
|
+
err.retryAfterMs = Math.max(0, when - Date.now());
|
|
468
|
+
}
|
|
469
|
+
}
|
|
416
470
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
471
|
+
} else if (res.status < 500) {
|
|
472
|
+
err.fatal = true; // genuine client error — do not retry
|
|
473
|
+
} // other 5xx: neither fatal nor throttle → ordinary bounded retry
|
|
474
|
+
return err;
|
|
421
475
|
}
|
|
422
476
|
/** GET a URL and parse JSON, with the shared retry policy: rate-limits (429/503)
|
|
423
477
|
* WAIT indefinitely (surfaced to the progress log via onThrottleWait, throttled
|
|
@@ -425,18 +479,25 @@ function httpError(res) {
|
|
|
425
479
|
* DOWNLOAD_TRIES. Used by every datasets-server API stage so all share the same
|
|
426
480
|
* never-drop-on-throttle behaviour. */
|
|
427
481
|
async function getJson(url, label) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
482
|
+
return retry(
|
|
483
|
+
label,
|
|
484
|
+
async () => {
|
|
485
|
+
const res = await fetch(url, { signal: shutdown.signal });
|
|
486
|
+
if (res.ok) {
|
|
487
|
+
return res.json();
|
|
488
|
+
}
|
|
489
|
+
throw httpError(res);
|
|
490
|
+
},
|
|
491
|
+
DOWNLOAD_TRIES,
|
|
492
|
+
undefined,
|
|
493
|
+
(ms) => {
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
if (onThrottleWait && now - lastThrottleLog > 3000) {
|
|
496
|
+
lastThrottleLog = now;
|
|
497
|
+
onThrottleWait(ms, label);
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
);
|
|
440
501
|
}
|
|
441
502
|
// ═══════════════════════════════════════════════════════════════════════
|
|
442
503
|
// §3 Cache + download helpers (a downloaded file is bounded by MAX_CACHE_GB)
|
|
@@ -448,144 +509,161 @@ async function getJson(url, label) {
|
|
|
448
509
|
/** A cheap HEAD to learn a download's size (for the cache ceiling and a real
|
|
449
510
|
* ETA). Rate-limits wait; other 4xx is fatal; total failure → 0. */
|
|
450
511
|
async function headSize(url) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
512
|
+
return retry(`HEAD ${url}`, async () => {
|
|
513
|
+
const res = await fetch(url, { method: "HEAD", signal: shutdown.signal });
|
|
514
|
+
if (res.ok) {
|
|
515
|
+
return Number(res.headers.get("content-length")) || 0;
|
|
516
|
+
}
|
|
517
|
+
throw httpError(res);
|
|
518
|
+
}, 4);
|
|
457
519
|
}
|
|
458
520
|
function cacheSize() {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
521
|
+
if (!existsSync(CACHE_DIR)) {
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
let total = 0;
|
|
525
|
+
for (const name of readdirSync(CACHE_DIR)) {
|
|
526
|
+
try {
|
|
527
|
+
total += statSync(join(CACHE_DIR, name)).size;
|
|
528
|
+
} catch { /* raced with a delete */ }
|
|
529
|
+
}
|
|
530
|
+
return total;
|
|
469
531
|
}
|
|
470
532
|
/** Block until there is room for a file of `fileBytes` under the ceiling.
|
|
471
533
|
* A single file larger than the whole ceiling can never "fit", so we let it
|
|
472
534
|
* through (it is deleted right after processing) rather than wait forever. */
|
|
473
535
|
async function ensureCacheRoom(fileBytes, warn) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
536
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
537
|
+
if (fileBytes >= MAX_CACHE_BYTES) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
let warned = false;
|
|
541
|
+
// Stop waiting the moment a shutdown is requested — the abort signal unblocks
|
|
542
|
+
// a long cache-full wait so Ctrl+C is never swallowed by the ceiling.
|
|
543
|
+
while (
|
|
544
|
+
!shutdown.signal.aborted && cacheSize() + fileBytes > MAX_CACHE_BYTES
|
|
545
|
+
) {
|
|
546
|
+
if (!warned) {
|
|
547
|
+
warn?.(
|
|
548
|
+
`${YEL}⚠${R} cache at ${
|
|
549
|
+
(MAX_CACHE_BYTES / 1e9).toFixed(0)
|
|
550
|
+
} GB ceiling — waiting for room…`,
|
|
551
|
+
);
|
|
552
|
+
warned = true;
|
|
486
553
|
}
|
|
554
|
+
await waitMs(5_000);
|
|
555
|
+
}
|
|
487
556
|
}
|
|
488
557
|
// ═══════════════════════════════════════════════════════════════════════
|
|
489
558
|
// §5 Download (streamed to disk, with retry + cleanup on failure)
|
|
490
559
|
// ═══════════════════════════════════════════════════════════════════════
|
|
491
|
-
async function downloadFile(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
off += writeSync(fd, chunk, off, chunk.length - off);
|
|
541
|
-
}
|
|
542
|
-
},
|
|
543
|
-
close() {
|
|
544
|
-
fsyncSync(fd); // durable bytes before the rename promotes them
|
|
545
|
-
closeFd();
|
|
546
|
-
},
|
|
547
|
-
abort() {
|
|
548
|
-
closeFd();
|
|
549
|
-
},
|
|
550
|
-
});
|
|
560
|
+
async function downloadFile(
|
|
561
|
+
url,
|
|
562
|
+
destPath,
|
|
563
|
+
tries = DOWNLOAD_TRIES,
|
|
564
|
+
onFail,
|
|
565
|
+
onProgress,
|
|
566
|
+
) {
|
|
567
|
+
const partPath = destPath + PART_SUFFIX;
|
|
568
|
+
await retry(
|
|
569
|
+
`download ${basename(destPath)}`,
|
|
570
|
+
async () => {
|
|
571
|
+
// Abort promptly on shutdown rather than waiting out a slow socket.
|
|
572
|
+
if (shutdown.signal.aborted) {
|
|
573
|
+
const e = new Error("aborted");
|
|
574
|
+
e.fatal = true;
|
|
575
|
+
throw e;
|
|
576
|
+
}
|
|
577
|
+
const res = await fetch(url, { signal: shutdown.signal });
|
|
578
|
+
if (!res.ok) {
|
|
579
|
+
throw httpError(res);
|
|
580
|
+
}
|
|
581
|
+
if (!res.body) {
|
|
582
|
+
throw new Error("empty response body");
|
|
583
|
+
}
|
|
584
|
+
const total = Number(res.headers.get("content-length")) || 0;
|
|
585
|
+
let done = 0;
|
|
586
|
+
// Stream straight to a ".part" sibling using pure WHATWG streams. A
|
|
587
|
+
// TransformStream meters progress; pipeTo into a WritableStream gives REAL
|
|
588
|
+
// backpressure natively — the sink's write() returns a promise the
|
|
589
|
+
// readable side awaits, so a fast server can never outrun the disk (no
|
|
590
|
+
// whole-file heap buffering). The sink wraps a single raw fs descriptor
|
|
591
|
+
// (the one capability the web platform lacks); writing to disk is the only
|
|
592
|
+
// Node operation in the whole pipeline. The final, valid file only ever
|
|
593
|
+
// appears via the atomic rename below, so a crash mid-transfer can never
|
|
594
|
+
// leave a truncated file at the real path.
|
|
595
|
+
const meter = new TransformStream({
|
|
596
|
+
transform(chunk, controller) {
|
|
597
|
+
done += chunk.length;
|
|
598
|
+
onProgress?.(done, total);
|
|
599
|
+
controller.enqueue(chunk);
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
const fd = openSync(partPath, "w");
|
|
603
|
+
let closed = false;
|
|
604
|
+
const closeFd = () => {
|
|
605
|
+
if (closed) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
closed = true;
|
|
551
609
|
try {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
610
|
+
closeSync(fd);
|
|
611
|
+
} catch { /* already closed */ }
|
|
612
|
+
};
|
|
613
|
+
const sink = new WritableStream({
|
|
614
|
+
write(chunk) {
|
|
615
|
+
// writeSync drains the whole chunk before returning, so the readable
|
|
616
|
+
// side is paused for exactly as long as the disk needs — backpressure.
|
|
617
|
+
let off = 0;
|
|
618
|
+
while (off < chunk.length) {
|
|
619
|
+
off += writeSync(fd, chunk, off, chunk.length - off);
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
close() {
|
|
623
|
+
fsyncSync(fd); // durable bytes before the rename promotes them
|
|
624
|
+
closeFd();
|
|
625
|
+
},
|
|
626
|
+
abort() {
|
|
627
|
+
closeFd();
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
try {
|
|
631
|
+
await res.body.pipeThrough(meter).pipeTo(sink, {
|
|
632
|
+
signal: shutdown.signal,
|
|
633
|
+
});
|
|
634
|
+
} catch (e) {
|
|
635
|
+
// pipeTo's abort() ran the sink's abort() (closing the descriptor); if
|
|
636
|
+
// it didn't (a non-abort throw), make sure the descriptor is not leaked.
|
|
637
|
+
closeFd();
|
|
569
638
|
try {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
639
|
+
unlinkSync(partPath);
|
|
640
|
+
} catch { /* best effort */ }
|
|
641
|
+
throw e;
|
|
642
|
+
}
|
|
643
|
+
// Optional integrity guard: when the server advertised a size, a complete
|
|
644
|
+
// file must match it. A short read (silent truncation) is retried rather
|
|
645
|
+
// than promoted, so the parser never sees a partial ZIP.
|
|
646
|
+
try {
|
|
647
|
+
const got = statSync(partPath).size;
|
|
648
|
+
if (total > 0 && got !== total) {
|
|
649
|
+
try {
|
|
650
|
+
unlinkSync(partPath);
|
|
651
|
+
} catch { /* best effort */ }
|
|
652
|
+
throw new Error(`size mismatch: got ${got}, expected ${total}`);
|
|
653
|
+
}
|
|
654
|
+
} catch (e) {
|
|
655
|
+
if (e instanceof Error && e.message.startsWith("size mismatch")) {
|
|
656
|
+
throw e;
|
|
657
|
+
}
|
|
658
|
+
// statSync failure is non-fatal here; the rename below will surface it.
|
|
659
|
+
}
|
|
660
|
+
// Atomic publish: rename is atomic within a filesystem, so the final path
|
|
661
|
+
// flips from "absent" to "complete" in one step — never an in-between.
|
|
662
|
+
renameSync(partPath, destPath);
|
|
663
|
+
},
|
|
664
|
+
tries,
|
|
665
|
+
onFail,
|
|
666
|
+
);
|
|
589
667
|
}
|
|
590
668
|
const isEpisode = (it) => typeof it !== "string";
|
|
591
669
|
/** Build the accumulated-context episodes of a turn sequence: each successive
|
|
@@ -593,109 +671,125 @@ const isEpisode = (it) => typeof it !== "string";
|
|
|
593
671
|
* the same cumulative-context shape a multi-turn conversation deposits, so the
|
|
594
672
|
* store learns to continue a growing context. */
|
|
595
673
|
function accumulate(turns) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
674
|
+
const out = [];
|
|
675
|
+
for (let i = 1; i < turns.length; i++) {
|
|
676
|
+
out.push({ context: turns.slice(0, i).join("\n"), continuation: turns[i] });
|
|
677
|
+
}
|
|
678
|
+
return out;
|
|
601
679
|
}
|
|
602
680
|
/** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
|
|
603
681
|
* repeats so a concept never deposits the same form twice. */
|
|
604
682
|
export function refineItems(items) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
seen.add(key);
|
|
613
|
-
out.push(exp);
|
|
614
|
-
}
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
const ctx = it.context.trim();
|
|
618
|
-
const cont = it.continuation.trim();
|
|
619
|
-
if (!ctx || !cont || ctx === cont)
|
|
620
|
-
continue;
|
|
621
|
-
const key = "P:" + ctx + "\u0000" + cont;
|
|
622
|
-
if (seen.has(key))
|
|
623
|
-
continue;
|
|
683
|
+
const out = [];
|
|
684
|
+
const seen = new Set();
|
|
685
|
+
for (const it of items) {
|
|
686
|
+
if (!isEpisode(it)) {
|
|
687
|
+
const exp = it.trim();
|
|
688
|
+
const key = "E:" + exp;
|
|
689
|
+
if (exp && !seen.has(key)) {
|
|
624
690
|
seen.add(key);
|
|
625
|
-
out.push(
|
|
691
|
+
out.push(exp);
|
|
692
|
+
}
|
|
693
|
+
continue;
|
|
626
694
|
}
|
|
627
|
-
|
|
695
|
+
const ctx = it.context.trim();
|
|
696
|
+
const cont = it.continuation.trim();
|
|
697
|
+
if (!ctx || !cont || ctx === cont) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const key = "P:" + ctx + "\u0000" + cont;
|
|
701
|
+
if (seen.has(key)) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
seen.add(key);
|
|
705
|
+
out.push({ context: ctx, continuation: cont });
|
|
706
|
+
}
|
|
707
|
+
return out;
|
|
628
708
|
}
|
|
629
709
|
/** Normalize a raw datasets-server row into a SmolSentRow, or null when it lacks
|
|
630
710
|
* both sides or a side is implausibly large (a dump, not a sentence). */
|
|
631
711
|
export function toSmolSentRow(row) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
712
|
+
if (!row || typeof row !== "object") {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const r = row;
|
|
716
|
+
const src = typeof r.src === "string" ? r.src.trim() : "";
|
|
717
|
+
// `trg` is a single string in smolsent; tolerate a list form defensively.
|
|
718
|
+
const trgRaw = Array.isArray(r.trgs) ? r.trgs[0] : r.trg;
|
|
719
|
+
const trg = typeof trgRaw === "string" ? trgRaw.trim() : "";
|
|
720
|
+
if (!src || !trg) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
if (src.length > MAX_SMOLSENT_CHARS || trg.length > MAX_SMOLSENT_CHARS) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const sl = typeof r.sl === "string" ? r.sl.trim() : "";
|
|
727
|
+
const tl = typeof r.tl === "string" ? r.tl.trim() : "";
|
|
728
|
+
return { src, trg, sl, tl };
|
|
646
729
|
}
|
|
647
730
|
/** Translate ONE SmolSent pair into SEMA facts: the two sentences are one
|
|
648
731
|
* meaning in two languages, so bind them BOTH ways. refineItems drops the
|
|
649
732
|
* degenerate case where src === trg. */
|
|
650
733
|
export function smolSentRowToItems(row) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
734
|
+
const { src, trg } = row;
|
|
735
|
+
return refineItems([
|
|
736
|
+
{ context: src, continuation: trg },
|
|
737
|
+
{ context: trg, continuation: src },
|
|
738
|
+
]);
|
|
656
739
|
}
|
|
657
740
|
/** Normalize a raw datasets-server row object into an AyaRow, or null when it
|
|
658
741
|
* lacks a usable prompt/answer or a field is implausibly large (a dump, not a
|
|
659
742
|
* cognitive example). Trims surrounding whitespace; keeps inner text verbatim
|
|
660
743
|
* (human prose, possibly multi-paragraph). */
|
|
661
744
|
export function toAyaRow(row) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
745
|
+
if (!row || typeof row !== "object") {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
const r = row;
|
|
749
|
+
const inputs = typeof r.inputs === "string" ? r.inputs.trim() : "";
|
|
750
|
+
const targets = typeof r.targets === "string" ? r.targets.trim() : "";
|
|
751
|
+
if (!inputs || !targets) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
if (
|
|
755
|
+
inputs.length > MAX_AYA_FIELD_CHARS || targets.length > MAX_AYA_FIELD_CHARS
|
|
756
|
+
) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
const language = typeof r.language === "string" ? r.language.trim() : "";
|
|
760
|
+
return { inputs, targets, language };
|
|
673
761
|
}
|
|
674
762
|
/** Translate ONE Aya row into SEMA training items. A row is a single human
|
|
675
763
|
* (question → answer) exchange — exactly one FACT, the (inputs → targets) edge.
|
|
676
764
|
* No standalone-answer experience and no one-exchange "cumulative" walk: a lone
|
|
677
765
|
* Q→A is not multi-turn, and both would only replicate the same edge. */
|
|
678
766
|
export function ayaRowToItems(row) {
|
|
679
|
-
|
|
680
|
-
|
|
767
|
+
const { inputs, targets } = row;
|
|
768
|
+
return refineItems([{ context: inputs, continuation: targets }]);
|
|
681
769
|
}
|
|
682
770
|
/** Collapse a conversation tree to ONE linear path: at each node, descend into
|
|
683
771
|
* its best-ranked, non-deleted reply (rank 0 preferred; unranked sorts last).
|
|
684
772
|
* Returns the ordered turns (already strictly alternating in this corpus). */
|
|
685
773
|
export function bestOasstPath(root) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
774
|
+
const turns = [];
|
|
775
|
+
let node = root;
|
|
776
|
+
while (node) {
|
|
777
|
+
const text = typeof node.text === "string" ? node.text.trim() : "";
|
|
778
|
+
if (text) {
|
|
779
|
+
turns.push({ role: String(node.role ?? "?"), text });
|
|
780
|
+
}
|
|
781
|
+
const live = (node.replies ?? []).filter((r) =>
|
|
782
|
+
r && !r.deleted && typeof r.text === "string" && r.text.trim() !== ""
|
|
783
|
+
);
|
|
784
|
+
if (live.length === 0) {
|
|
785
|
+
break;
|
|
697
786
|
}
|
|
698
|
-
|
|
787
|
+
live.sort((a, b) =>
|
|
788
|
+
(a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER)
|
|
789
|
+
);
|
|
790
|
+
node = live[0];
|
|
791
|
+
}
|
|
792
|
+
return turns;
|
|
699
793
|
}
|
|
700
794
|
/** Translate ONE multi-turn oasst2 conversation into SEMA training items.
|
|
701
795
|
*
|
|
@@ -717,41 +811,47 @@ export function bestOasstPath(root) {
|
|
|
717
811
|
* Returns [] for a conversation below the multi-turn threshold, so callers can
|
|
718
812
|
* simply skip empties. */
|
|
719
813
|
export function oasstConversationToItems(turns) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
814
|
+
if (turns.length < OASST_MIN_TURNS) {
|
|
815
|
+
return []; // not multi-turn — skip
|
|
816
|
+
}
|
|
817
|
+
return refineItems(accumulate(turns.map((t) => t.text)));
|
|
723
818
|
}
|
|
724
819
|
/** Turn a source value into clean prose: decode the literal "\n"/"\t"/"\r"
|
|
725
820
|
* two-character escapes the source JSON left in the text, collapse the runs of
|
|
726
821
|
* whitespace that creates, and trim. */
|
|
727
822
|
function unescapePlain(s) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
823
|
+
return s
|
|
824
|
+
.replace(/\\r\\n|\\n|\\r/g, "\n")
|
|
825
|
+
.replace(/\\t/g, " ")
|
|
826
|
+
.replace(/[ \t]+/g, " ")
|
|
827
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
828
|
+
.trim();
|
|
734
829
|
}
|
|
735
830
|
/** Normalize a raw datasets-server row into a GenKnowRow, or null when it lacks
|
|
736
831
|
* a usable question/answer or a side is implausibly large (corruption). */
|
|
737
832
|
export function toGenKnowRow(row) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
833
|
+
if (!row || typeof row !== "object") {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
const r = row;
|
|
837
|
+
const question = typeof r.Question === "string"
|
|
838
|
+
? unescapePlain(r.Question)
|
|
839
|
+
: "";
|
|
840
|
+
const answer = typeof r.Answer === "string" ? unescapePlain(r.Answer) : "";
|
|
841
|
+
if (!question || !answer) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
if (
|
|
845
|
+
question.length > MAX_GENKNOW_CHARS || answer.length > MAX_GENKNOW_CHARS
|
|
846
|
+
) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
return { question, answer };
|
|
750
850
|
}
|
|
751
851
|
/** Translate ONE General-Knowledge row into SEMA items: exactly one
|
|
752
852
|
* (question → answer) FACT. refineItems drops a degenerate question === answer. */
|
|
753
853
|
export function genKnowRowToItems(row) {
|
|
754
|
-
|
|
854
|
+
return refineItems([{ context: row.question, continuation: row.answer }]);
|
|
755
855
|
}
|
|
756
856
|
// ═══════════════════════════════════════════════════════════════════════
|
|
757
857
|
// §7 Ingestion
|
|
@@ -767,20 +867,23 @@ const ENC = new TextEncoder();
|
|
|
767
867
|
/** Content size of a training item in UTF-8 bytes — the same quantity the
|
|
768
868
|
* scaling suite (14-scaling.test.mjs) measures as KB/s: for an episode the
|
|
769
869
|
* context plus the continuation, for a bare experience its own text. */
|
|
770
|
-
const itemBytes = (it) =>
|
|
870
|
+
const itemBytes = (it) =>
|
|
871
|
+
isEpisode(it)
|
|
771
872
|
? ENC.encode(it.context).length + ENC.encode(it.continuation).length
|
|
772
873
|
: ENC.encode(it).length;
|
|
773
874
|
async function ingestItems(ci, items, onItem, sample) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
875
|
+
for (const it of items) {
|
|
876
|
+
if (isEpisode(it)) {
|
|
877
|
+
await ci.ingest(it.context, it.continuation);
|
|
878
|
+
} else {
|
|
879
|
+
await ci.ingest(it);
|
|
880
|
+
}
|
|
881
|
+
sample?.(it);
|
|
882
|
+
if (!(await onItem(itemBytes(it)))) {
|
|
883
|
+
return false; // stop requested
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return true;
|
|
784
887
|
}
|
|
785
888
|
// ── §7a′ oasst2 — stream the gzipped JSONL of trees and deposit multi-turn ──
|
|
786
889
|
//
|
|
@@ -791,462 +894,495 @@ async function ingestItems(ci, items, onItem, sample) {
|
|
|
791
894
|
// line that fails to parse (or is oversize) is counted skipped and the stream
|
|
792
895
|
// continues; a cap/signal stops cleanly at a conversation boundary.
|
|
793
896
|
async function processOasst(filePath, ci, onExample, sample) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
tree = JSON.parse(line);
|
|
810
|
-
}
|
|
811
|
-
catch {
|
|
812
|
-
skipped++;
|
|
813
|
-
return true;
|
|
814
|
-
}
|
|
815
|
-
if (!tree.prompt)
|
|
816
|
-
return true;
|
|
817
|
-
const turns = bestOasstPath(tree.prompt);
|
|
818
|
-
const items = oasstConversationToItems(turns); // [] when not multi-turn
|
|
819
|
-
if (items.length === 0)
|
|
820
|
-
return true; // single-turn / empty — skipped
|
|
821
|
-
multi++;
|
|
822
|
-
return ingestItems(ci, items, async (contentBytes) => {
|
|
823
|
-
examples++;
|
|
824
|
-
return onExample(contentBytes);
|
|
825
|
-
}, sample);
|
|
826
|
-
};
|
|
897
|
+
const blob = await openAsBlob(filePath);
|
|
898
|
+
const reader = blob.stream()
|
|
899
|
+
.pipeThrough(new DecompressionStream("gzip"))
|
|
900
|
+
.pipeThrough(new TextDecoderStream())
|
|
901
|
+
.getReader();
|
|
902
|
+
let examples = 0;
|
|
903
|
+
let skipped = 0; // malformed/oversize lines
|
|
904
|
+
let multi = 0; // multi-turn conversations deposited
|
|
905
|
+
let leftover = "";
|
|
906
|
+
let droppingLine = false;
|
|
907
|
+
const processLine = async (line) => {
|
|
908
|
+
if (!line.trim()) {
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
let tree;
|
|
827
912
|
try {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (!droppingLine && leftover.trim()) {
|
|
867
|
-
if (!(await processLine(leftover))) {
|
|
868
|
-
return { examples, stopped: true, skipped, multi };
|
|
913
|
+
tree = JSON.parse(line);
|
|
914
|
+
} catch {
|
|
915
|
+
skipped++;
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
if (!tree.prompt) {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
const turns = bestOasstPath(tree.prompt);
|
|
922
|
+
const items = oasstConversationToItems(turns); // [] when not multi-turn
|
|
923
|
+
if (items.length === 0) {
|
|
924
|
+
return true; // single-turn / empty — skipped
|
|
925
|
+
}
|
|
926
|
+
multi++;
|
|
927
|
+
return ingestItems(ci, items, async (contentBytes) => {
|
|
928
|
+
examples++;
|
|
929
|
+
return onExample(contentBytes);
|
|
930
|
+
}, sample);
|
|
931
|
+
};
|
|
932
|
+
try {
|
|
933
|
+
while (true) {
|
|
934
|
+
const { done, value } = await reader.read();
|
|
935
|
+
if (done) {
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
let chunk = value;
|
|
939
|
+
for (;;) {
|
|
940
|
+
const nl = chunk.indexOf("\n");
|
|
941
|
+
if (nl < 0) {
|
|
942
|
+
if (!droppingLine) {
|
|
943
|
+
if (leftover.length + chunk.length > MAX_OASST_LINE_CHARS) {
|
|
944
|
+
leftover = "";
|
|
945
|
+
droppingLine = true;
|
|
946
|
+
skipped++;
|
|
947
|
+
} else {
|
|
948
|
+
leftover += chunk;
|
|
869
949
|
}
|
|
870
|
-
|
|
871
|
-
|
|
950
|
+
}
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
const part = chunk.slice(0, nl);
|
|
954
|
+
chunk = chunk.slice(nl + 1);
|
|
955
|
+
if (droppingLine) {
|
|
956
|
+
droppingLine = false;
|
|
957
|
+
leftover = "";
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (leftover.length + part.length > MAX_OASST_LINE_CHARS) {
|
|
961
|
+
leftover = "";
|
|
962
|
+
skipped++;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const line = leftover + part;
|
|
966
|
+
leftover = "";
|
|
967
|
+
if (!(await processLine(line))) {
|
|
968
|
+
return { examples, stopped: true, skipped, multi };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
872
971
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
catch { /* best effort */ }
|
|
972
|
+
if (!droppingLine && leftover.trim()) {
|
|
973
|
+
if (!(await processLine(leftover))) {
|
|
974
|
+
return { examples, stopped: true, skipped, multi };
|
|
975
|
+
}
|
|
878
976
|
}
|
|
977
|
+
return { examples, stopped: false, skipped, multi };
|
|
978
|
+
} finally {
|
|
979
|
+
try {
|
|
980
|
+
reader.releaseLock();
|
|
981
|
+
} catch { /* best effort */ }
|
|
982
|
+
}
|
|
879
983
|
}
|
|
880
984
|
/** Discover the SmolSent per-pair JSONL files from the HF repo tree, restricted
|
|
881
985
|
* to SMOLSENT_PAIRS (basenames without .jsonl) when set. Each entry is the
|
|
882
986
|
* repo-relative path, e.g. "smolsent/ha_en.jsonl". */
|
|
883
987
|
async function listSmolSentFiles() {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
988
|
+
// The dataset id ("owner/name") is a PATH here, so its "/" must not be
|
|
989
|
+
// percent-encoded. `recursive=true` returns every file under smolsent/.
|
|
990
|
+
const url = `https://huggingface.co/api/datasets/${SMOLSENT_DATASET}` +
|
|
991
|
+
`/tree/main/smolsent?recursive=true`;
|
|
992
|
+
const body = await getJson(url, `GET smol tree`);
|
|
993
|
+
const paths = Array.isArray(body)
|
|
994
|
+
? body
|
|
995
|
+
.filter((e) => e?.type === "file" && /\.jsonl$/i.test(e?.path))
|
|
996
|
+
.map((e) => String(e.path))
|
|
997
|
+
: [];
|
|
998
|
+
paths.sort();
|
|
999
|
+
if (!SMOLSENT_PAIRS.length) {
|
|
1000
|
+
return paths;
|
|
1001
|
+
}
|
|
1002
|
+
const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
|
|
1003
|
+
return paths.filter((p) => want.has(basename(p).replace(/\.jsonl$/i, "")));
|
|
899
1004
|
}
|
|
900
1005
|
/** Stream a plain-JSONL file from disk, deposit each parsed row via `toItems`.
|
|
901
1006
|
* Lines are split without buffering the whole file; an oversize/malformed line
|
|
902
1007
|
* is counted skipped and the stream continues. Shared by SmolSent (and any
|
|
903
1008
|
* future JSONL source). */
|
|
904
|
-
async function processJsonl(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if (!items || items.length === 0) {
|
|
921
|
-
skipped++;
|
|
922
|
-
return true;
|
|
923
|
-
}
|
|
924
|
-
return ingestItems(ci, items, async (contentBytes) => {
|
|
925
|
-
examples++;
|
|
926
|
-
return onExample(contentBytes);
|
|
927
|
-
}, sample);
|
|
928
|
-
};
|
|
1009
|
+
async function processJsonl(
|
|
1010
|
+
filePath,
|
|
1011
|
+
toItems,
|
|
1012
|
+
ci,
|
|
1013
|
+
onExample,
|
|
1014
|
+
sample,
|
|
1015
|
+
maxLineChars,
|
|
1016
|
+
) {
|
|
1017
|
+
const blob = await openAsBlob(filePath);
|
|
1018
|
+
const reader = blob.stream().pipeThrough(new TextDecoderStream()).getReader();
|
|
1019
|
+
let examples = 0, skipped = 0, leftover = "", dropping = false;
|
|
1020
|
+
const processLine = async (line) => {
|
|
1021
|
+
if (!line.trim()) {
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
let row;
|
|
929
1025
|
try {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
leftover = "";
|
|
963
|
-
if (!(await processLine(line))) {
|
|
964
|
-
return { examples, stopped: true, skipped };
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
if (!dropping && leftover.trim()) {
|
|
969
|
-
if (!(await processLine(leftover))) {
|
|
970
|
-
return { examples, stopped: true, skipped };
|
|
1026
|
+
row = JSON.parse(line);
|
|
1027
|
+
} catch {
|
|
1028
|
+
skipped++;
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
const items = toItems(row);
|
|
1032
|
+
if (!items || items.length === 0) {
|
|
1033
|
+
skipped++;
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
return ingestItems(ci, items, async (contentBytes) => {
|
|
1037
|
+
examples++;
|
|
1038
|
+
return onExample(contentBytes);
|
|
1039
|
+
}, sample);
|
|
1040
|
+
};
|
|
1041
|
+
try {
|
|
1042
|
+
while (true) {
|
|
1043
|
+
const { done, value } = await reader.read();
|
|
1044
|
+
if (done) {
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
let chunk = value;
|
|
1048
|
+
for (;;) {
|
|
1049
|
+
const nl = chunk.indexOf("\n");
|
|
1050
|
+
if (nl < 0) {
|
|
1051
|
+
if (!dropping) {
|
|
1052
|
+
if (leftover.length + chunk.length > maxLineChars) {
|
|
1053
|
+
leftover = "";
|
|
1054
|
+
dropping = true;
|
|
1055
|
+
skipped++;
|
|
1056
|
+
} else {
|
|
1057
|
+
leftover += chunk;
|
|
971
1058
|
}
|
|
972
|
-
|
|
973
|
-
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
const part = chunk.slice(0, nl);
|
|
1063
|
+
chunk = chunk.slice(nl + 1);
|
|
1064
|
+
if (dropping) {
|
|
1065
|
+
dropping = false;
|
|
1066
|
+
leftover = "";
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (leftover.length + part.length > maxLineChars) {
|
|
1070
|
+
leftover = "";
|
|
1071
|
+
skipped++;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const line = leftover + part;
|
|
1075
|
+
leftover = "";
|
|
1076
|
+
if (!(await processLine(line))) {
|
|
1077
|
+
return { examples, stopped: true, skipped };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
974
1080
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
catch { /* best effort */ }
|
|
1081
|
+
if (!dropping && leftover.trim()) {
|
|
1082
|
+
if (!(await processLine(leftover))) {
|
|
1083
|
+
return { examples, stopped: true, skipped };
|
|
1084
|
+
}
|
|
980
1085
|
}
|
|
1086
|
+
return { examples, stopped: false, skipped };
|
|
1087
|
+
} finally {
|
|
1088
|
+
try {
|
|
1089
|
+
reader.releaseLock();
|
|
1090
|
+
} catch { /* best effort */ }
|
|
1091
|
+
}
|
|
981
1092
|
}
|
|
982
1093
|
/** Read a downloaded Parquet file row-group by row-group with hyparquet (+Snappy
|
|
983
1094
|
* from hyparquet-compressors) over a web-standard Blob byte source, depositing
|
|
984
1095
|
* each row via `toItems`. Only one row-group is materialised at a time, so a
|
|
985
1096
|
* multi-hundred-MB file never loads whole into memory. */
|
|
986
1097
|
async function processParquet(filePath, toItems, ci, onExample, sample) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const rowEnd = rowStart + rgRows;
|
|
1000
|
-
// Materialise exactly one row-group, then deposit its rows.
|
|
1001
|
-
const rows = await parquetReadObjects({
|
|
1002
|
-
file,
|
|
1003
|
-
compressors,
|
|
1004
|
-
rowStart,
|
|
1005
|
-
rowEnd,
|
|
1006
|
-
});
|
|
1007
|
-
rowStart = rowEnd;
|
|
1008
|
-
for (const row of rows) {
|
|
1009
|
-
const items = toItems(row);
|
|
1010
|
-
if (!items || items.length === 0) {
|
|
1011
|
-
skipped++;
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1015
|
-
examples++;
|
|
1016
|
-
return onExample(contentBytes);
|
|
1017
|
-
}, sample);
|
|
1018
|
-
if (!ok)
|
|
1019
|
-
return { examples, stopped: true, skipped };
|
|
1020
|
-
}
|
|
1098
|
+
const blob = await openAsBlob(filePath);
|
|
1099
|
+
const file = {
|
|
1100
|
+
byteLength: blob.size,
|
|
1101
|
+
slice: async (start, end) =>
|
|
1102
|
+
await blob.slice(start, end ?? blob.size).arrayBuffer(),
|
|
1103
|
+
};
|
|
1104
|
+
const meta = await parquetMetadataAsync(file);
|
|
1105
|
+
let examples = 0, skipped = 0;
|
|
1106
|
+
let rowStart = 0;
|
|
1107
|
+
for (const rg of meta.row_groups) {
|
|
1108
|
+
if (shutdown.signal.aborted) {
|
|
1109
|
+
return { examples, stopped: true, skipped };
|
|
1021
1110
|
}
|
|
1022
|
-
|
|
1111
|
+
const rgRows = Number(rg.num_rows);
|
|
1112
|
+
const rowEnd = rowStart + rgRows;
|
|
1113
|
+
// Materialise exactly one row-group, then deposit its rows.
|
|
1114
|
+
const rows = await parquetReadObjects({
|
|
1115
|
+
file,
|
|
1116
|
+
compressors,
|
|
1117
|
+
rowStart,
|
|
1118
|
+
rowEnd,
|
|
1119
|
+
});
|
|
1120
|
+
rowStart = rowEnd;
|
|
1121
|
+
for (const row of rows) {
|
|
1122
|
+
const items = toItems(row);
|
|
1123
|
+
if (!items || items.length === 0) {
|
|
1124
|
+
skipped++;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1128
|
+
examples++;
|
|
1129
|
+
return onExample(contentBytes);
|
|
1130
|
+
}, sample);
|
|
1131
|
+
if (!ok) {
|
|
1132
|
+
return { examples, stopped: true, skipped };
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return { examples, stopped: false, skipped };
|
|
1023
1137
|
}
|
|
1024
1138
|
/** Read a downloaded JSON-array file (General-Knowledge output.json) and deposit
|
|
1025
1139
|
* each element via `toItems`. The array is small enough (~16 MB) to parse whole;
|
|
1026
1140
|
* a huge file would be rejected by the cache ceiling long before this. */
|
|
1027
1141
|
async function processJsonArray(filePath, toItems, ci, onExample, sample) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1142
|
+
const blob = await openAsBlob(filePath);
|
|
1143
|
+
let arr;
|
|
1144
|
+
try {
|
|
1145
|
+
arr = JSON.parse(await blob.text());
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
throw new Error(`invalid JSON: ${e.message}`);
|
|
1148
|
+
}
|
|
1149
|
+
const rows = Array.isArray(arr) ? arr : [];
|
|
1150
|
+
let examples = 0, skipped = 0;
|
|
1151
|
+
for (const row of rows) {
|
|
1152
|
+
if (shutdown.signal.aborted) {
|
|
1153
|
+
return { examples, stopped: true, skipped };
|
|
1032
1154
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1155
|
+
const items = toItems(row);
|
|
1156
|
+
if (!items || items.length === 0) {
|
|
1157
|
+
skipped++;
|
|
1158
|
+
continue;
|
|
1035
1159
|
}
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (!items || items.length === 0) {
|
|
1043
|
-
skipped++;
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1047
|
-
examples++;
|
|
1048
|
-
return onExample(contentBytes);
|
|
1049
|
-
}, sample);
|
|
1050
|
-
if (!ok)
|
|
1051
|
-
return { examples, stopped: true, skipped };
|
|
1160
|
+
const ok = await ingestItems(ci, items, async (contentBytes) => {
|
|
1161
|
+
examples++;
|
|
1162
|
+
return onExample(contentBytes);
|
|
1163
|
+
}, sample);
|
|
1164
|
+
if (!ok) {
|
|
1165
|
+
return { examples, stopped: true, skipped };
|
|
1052
1166
|
}
|
|
1053
|
-
|
|
1167
|
+
}
|
|
1168
|
+
return { examples, stopped: false, skipped };
|
|
1054
1169
|
}
|
|
1055
1170
|
/** A prompt/expected pair to display for an item. */
|
|
1056
1171
|
function promptOf(it) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1172
|
+
return isEpisode(it)
|
|
1173
|
+
? { prompt: it.context, expected: it.continuation, kind: "episode" }
|
|
1174
|
+
: { prompt: it.slice(0, 200), expected: null, kind: "experience" };
|
|
1060
1175
|
}
|
|
1061
1176
|
/** A coarse, honest similarity between an expected continuation and SEMA's
|
|
1062
1177
|
* recall. Both are normalized (lowercased, whitespace-collapsed) and compared
|
|
1063
1178
|
* by the longest shared leading run plus token overlap, so the verdict is a
|
|
1064
1179
|
* heuristic signal of recall quality rather than a brittle fixed-prefix test. */
|
|
1065
1180
|
function recallSimilarity(expected, response) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1181
|
+
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1182
|
+
const a = norm(expected), b = norm(response);
|
|
1183
|
+
if (!a || !b) {
|
|
1184
|
+
return 0;
|
|
1185
|
+
}
|
|
1186
|
+
let lead = 0;
|
|
1187
|
+
const lim = Math.min(a.length, b.length);
|
|
1188
|
+
while (lead < lim && a[lead] === b[lead]) {
|
|
1189
|
+
lead++;
|
|
1190
|
+
}
|
|
1191
|
+
const leadFrac = lead / Math.max(1, Math.min(a.length, b.length));
|
|
1192
|
+
const ta = new Set(a.split(" ")), tb = new Set(b.split(" "));
|
|
1193
|
+
let inter = 0;
|
|
1194
|
+
for (const w of ta) {
|
|
1195
|
+
if (tb.has(w)) {
|
|
1196
|
+
inter++;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const jac = inter / Math.max(1, ta.size + tb.size - inter);
|
|
1200
|
+
return Math.max(leadFrac, jac);
|
|
1082
1201
|
}
|
|
1083
1202
|
/** A framed recall sample. Pinned in the panel on a TTY (so the most recent
|
|
1084
1203
|
* example is always on screen) and logged once per checkpoint when piped. */
|
|
1085
1204
|
function renderInferenceBox(prompt, expected, response, kind, checkpointN) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
return lines.join("\n");
|
|
1205
|
+
const W = 68;
|
|
1206
|
+
const hr = `${DIM}${"─".repeat(W)}${R}`;
|
|
1207
|
+
const title = kind === "episode"
|
|
1208
|
+
? "latest recall"
|
|
1209
|
+
: "latest recall (experience)";
|
|
1210
|
+
const head = `${title} · checkpoint #${checkpointN} `;
|
|
1211
|
+
const shown = response.trim() ? response : "(empty)";
|
|
1212
|
+
const lines = [
|
|
1213
|
+
`${B}╭─ ${head}${"─".repeat(Math.max(0, W - 2 - head.length))}╮${R}`,
|
|
1214
|
+
`${B}│${R} ${hr}`,
|
|
1215
|
+
`${B}│${R} ${CYAN}${B}Context:${R} ${clip(prompt, W - 13)}`,
|
|
1216
|
+
];
|
|
1217
|
+
if (expected) {
|
|
1218
|
+
lines.push(`${B}│${R} ${YEL}${B}Expected:${R} ${clip(expected, W - 13)}`);
|
|
1219
|
+
}
|
|
1220
|
+
lines.push(`${B}│${R} ${GRN}${B}SEMA:${R} ${clip(shown, W - 13)}`);
|
|
1221
|
+
lines.push(`${B}│${R} ${hr}`);
|
|
1222
|
+
let verdict;
|
|
1223
|
+
if (expected) {
|
|
1224
|
+
const sim = recallSimilarity(expected, response);
|
|
1225
|
+
const pctStr = `${Math.round(sim * 100)}%`;
|
|
1226
|
+
verdict = sim >= 0.6
|
|
1227
|
+
? `${GRN}✓${R} recall close to expected ${DIM}(~${pctStr} overlap)${R}`
|
|
1228
|
+
: sim >= 0.25
|
|
1229
|
+
? `${YEL}△${R} partial recall ${DIM}(~${pctStr} overlap)${R}`
|
|
1230
|
+
: `${RED}✗${R} recall diverges ${DIM}(~${pctStr} overlap)${R}`;
|
|
1231
|
+
} else {
|
|
1232
|
+
verdict = `${DIM}·${R} plain experience — no expected answer`;
|
|
1233
|
+
}
|
|
1234
|
+
lines.push(`${B}│${R} ${verdict}`);
|
|
1235
|
+
lines.push(`${B}╰${"─".repeat(W)}╯${R}`);
|
|
1236
|
+
return lines.join("\n");
|
|
1119
1237
|
}
|
|
1120
1238
|
function renderPanel(s) {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1137
|
-
return "∞";
|
|
1138
|
-
})();
|
|
1139
|
-
const fileFrac = s.fileTotal > 0 ? s.fileIndex / s.fileTotal : 0;
|
|
1140
|
-
let actIcon = `${DIM}·${R}`, actText = "waiting…";
|
|
1141
|
-
if (s.activity === "download") {
|
|
1142
|
-
actIcon = `${CYAN}⬇${R}`;
|
|
1143
|
-
const name = s.filePath;
|
|
1144
|
-
const total = s.dlTotal > 0 ? s.dlTotal : s.fileSize;
|
|
1145
|
-
if (total > 0 && s.dlDone > 0) {
|
|
1146
|
-
const dlFrac = clamp01(s.dlDone / total);
|
|
1147
|
-
actText =
|
|
1148
|
-
`downloading ${name} ${bar(18, dlFrac)} ${B}${pct(dlFrac)}${R}` +
|
|
1149
|
-
` ${DIM}${bytes(s.dlDone)}/${bytes(total)}${R}`;
|
|
1150
|
-
if (s.dlSpeed > 0)
|
|
1151
|
-
actText += ` ${DIM}@ ${bytes(s.dlSpeed)}/s${R}`;
|
|
1152
|
-
}
|
|
1153
|
-
else {
|
|
1154
|
-
actText = total > 0
|
|
1155
|
-
? `downloading ${name} · ${bytes(total)}…`
|
|
1156
|
-
: `downloading ${name}…`;
|
|
1157
|
-
}
|
|
1239
|
+
const targetKnown = isFinite(s.target);
|
|
1240
|
+
// Primary progress: by learned-content bytes when a MAX_MB target is set,
|
|
1241
|
+
// else by how far we are through the corpus on disk (bytes) — so the default
|
|
1242
|
+
// unbounded run still shows a real fraction and a real ETA.
|
|
1243
|
+
const frac = targetKnown
|
|
1244
|
+
? (s.target > 0 ? s.trainedBytes / s.target : 0)
|
|
1245
|
+
: (s.bytesTotal > 0 ? s.bytesDone / s.bytesTotal : 0);
|
|
1246
|
+
const etaStr = (() => {
|
|
1247
|
+
if (targetKnown) {
|
|
1248
|
+
return s.trainedRate > 0
|
|
1249
|
+
? dur((s.target - s.trainedBytes) / s.trainedRate)
|
|
1250
|
+
: "∞";
|
|
1251
|
+
}
|
|
1252
|
+
if (s.bytesTotal > 0 && s.bytesRate > 0) {
|
|
1253
|
+
return dur((s.bytesTotal - s.bytesDone) / s.bytesRate);
|
|
1158
1254
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1255
|
+
return "∞";
|
|
1256
|
+
})();
|
|
1257
|
+
const fileFrac = s.fileTotal > 0 ? s.fileIndex / s.fileTotal : 0;
|
|
1258
|
+
let actIcon = `${DIM}·${R}`, actText = "waiting…";
|
|
1259
|
+
if (s.activity === "download") {
|
|
1260
|
+
actIcon = `${CYAN}⬇${R}`;
|
|
1261
|
+
const name = s.filePath;
|
|
1262
|
+
const total = s.dlTotal > 0 ? s.dlTotal : s.fileSize;
|
|
1263
|
+
if (total > 0 && s.dlDone > 0) {
|
|
1264
|
+
const dlFrac = clamp01(s.dlDone / total);
|
|
1265
|
+
actText =
|
|
1266
|
+
`downloading ${name} ${bar(18, dlFrac)} ${B}${pct(dlFrac)}${R}` +
|
|
1267
|
+
` ${DIM}${bytes(s.dlDone)}/${bytes(total)}${R}`;
|
|
1268
|
+
if (s.dlSpeed > 0) {
|
|
1269
|
+
actText += ` ${DIM}@ ${bytes(s.dlSpeed)}/s${R}`;
|
|
1270
|
+
}
|
|
1271
|
+
} else {
|
|
1272
|
+
actText = total > 0
|
|
1273
|
+
? `downloading ${name} · ${bytes(total)}…`
|
|
1274
|
+
: `downloading ${name}…`;
|
|
1275
|
+
}
|
|
1276
|
+
} else if (s.activity === "process") {
|
|
1277
|
+
actIcon = `${GRN}✓${R}`;
|
|
1278
|
+
actText = `processing ${s.filePath} · ${
|
|
1279
|
+
int(s.fileExamples)
|
|
1280
|
+
} examples so far`;
|
|
1281
|
+
}
|
|
1282
|
+
const targetStr = targetKnown ? bytes(s.target) : "∞";
|
|
1283
|
+
const headExamples = targetKnown
|
|
1284
|
+
? `${CYAN}${bytes(s.trainedBytes)}${R} / ${targetStr} learned ${DIM}·${R} ${
|
|
1285
|
+
int(s.exampleCount)
|
|
1286
|
+
} examples`
|
|
1287
|
+
: `${CYAN}${int(s.exampleCount)}${R} examples`;
|
|
1288
|
+
const corpusInfo = s.bytesTotal > 0
|
|
1289
|
+
? `${B}📦${R} ${bytes(s.bytesDone)}/${bytes(s.bytesTotal)} (${
|
|
1290
|
+
pct(s.bytesDone / s.bytesTotal)
|
|
1291
|
+
})`
|
|
1292
|
+
: `${B}📦${R} ${bytes(s.bytesDone)} processed`;
|
|
1293
|
+
const fileInfo = s.fileTotal > 0
|
|
1294
|
+
? `${B}🌐${R} ${s.fileIndex}/${s.fileTotal} (${pct(fileFrac)})`
|
|
1295
|
+
: `${B}🌐${R} ${s.fileIndex} languages`;
|
|
1296
|
+
const panel = [
|
|
1297
|
+
`${B}╭${R}${B} sema train${R} ${DIM}·${R} SmolSent+Aya+oasst2 ${DIM}·${R} ` +
|
|
1298
|
+
`D=${D} ${DIM}·${R} seed=${SEED} ${DIM}·${R} ` +
|
|
1299
|
+
`store=${
|
|
1300
|
+
basename(DB_PATH)
|
|
1301
|
+
}.sqlite\n${B}╰${R} target=${CYAN}${targetStr}${R} ` +
|
|
1302
|
+
`learned ${DIM}·${R} checkpoint every ${bytes(CHECKPOINT_BYTES)}`,
|
|
1303
|
+
`\n${bar(40, frac)} ${B}${pct(frac)}${R} ${headExamples}`,
|
|
1304
|
+
`\n${B}⚡${R} ${bytes(s.trainedRate)}/s learned ${B}🧠${R} ${
|
|
1305
|
+
bytes(s.trainedBytes)
|
|
1306
|
+
} content ${B}⏱${R} ${dur(s.elapsedS)} elapsed ${B}🕐${R} ${etaStr} ETA`,
|
|
1307
|
+
`${fileInfo} ${corpusInfo} ${B}🗄${R} ${num(s.storeEntries)} entries ` +
|
|
1308
|
+
`${B}💾${R} cache ${bytes(s.cacheBytes)}`,
|
|
1309
|
+
`\n${actIcon} ${actText}`,
|
|
1310
|
+
].join("");
|
|
1311
|
+
return s.lastSample ? `${panel}\n${s.lastSample}` : panel;
|
|
1185
1312
|
}
|
|
1186
1313
|
/** A live panel pinned to the bottom of stderr. On a TTY it redraws in place,
|
|
1187
1314
|
* clearing only its own lines; logs are flushed into the scrollback above it.
|
|
1188
1315
|
* Off a TTY (piped/CI) the panel is suppressed and a plain status line is
|
|
1189
1316
|
* emitted occasionally, so logs stay clean and parseable. */
|
|
1190
1317
|
class Progress {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1318
|
+
lines = 0; // height of the panel currently on screen
|
|
1319
|
+
lastPaint = 0;
|
|
1320
|
+
lastStatus = 0;
|
|
1321
|
+
last = null;
|
|
1322
|
+
tty = process.stderr.isTTY === true;
|
|
1323
|
+
/** True when attached to an interactive terminal (panel is live). */
|
|
1324
|
+
get interactive() {
|
|
1325
|
+
return this.tty;
|
|
1326
|
+
}
|
|
1327
|
+
/** Cursor sequence that returns to the top of the panel and clears it. */
|
|
1328
|
+
clearPanel() {
|
|
1329
|
+
if (this.lines <= 0) {
|
|
1330
|
+
return "";
|
|
1331
|
+
}
|
|
1332
|
+
const up = this.lines - 1; // cursor is on the panel's last line
|
|
1333
|
+
return (up > 0 ? `${CSI}${up}F` : "\r") + `${CSI}0J`;
|
|
1334
|
+
}
|
|
1335
|
+
render(s, force = false) {
|
|
1336
|
+
this.last = s;
|
|
1337
|
+
const now = Date.now();
|
|
1338
|
+
if (!force && now - this.lastPaint < PROGRESS_MS) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
this.lastPaint = now;
|
|
1342
|
+
if (!this.tty) {
|
|
1343
|
+
if (force || now - this.lastStatus >= 10_000) {
|
|
1344
|
+
this.lastStatus = now;
|
|
1345
|
+
const targetKnown = isFinite(s.target);
|
|
1346
|
+
const where = s.bytesTotal > 0
|
|
1347
|
+
? ` ${pct(s.bytesDone / s.bytesTotal)} of corpus`
|
|
1348
|
+
: "";
|
|
1349
|
+
process.stderr.write(
|
|
1350
|
+
`[sema] ${bytes(s.trainedBytes)}${
|
|
1351
|
+
targetKnown ? "/" + bytes(s.target) : ""
|
|
1352
|
+
} learned · ${int(s.exampleCount)} examples · ` +
|
|
1353
|
+
`${
|
|
1354
|
+
bytes(s.trainedRate)
|
|
1355
|
+
}/s · lang ${s.fileIndex}/${s.fileTotal}${where} · ` +
|
|
1356
|
+
`${num(s.storeEntries)} entries\n`,
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const text = renderPanel(s);
|
|
1362
|
+
process.stderr.write(`${this.clearPanel()}${HIDE}${text}`);
|
|
1363
|
+
this.lines = text.split("\n").length;
|
|
1364
|
+
}
|
|
1365
|
+
/** Emit a line (or block) into the scrollback above the panel; the panel is
|
|
1366
|
+
* redrawn immediately beneath it so it never disappears between frames. */
|
|
1367
|
+
log(msg) {
|
|
1368
|
+
if (!this.tty) {
|
|
1369
|
+
process.stderr.write(`${msg}\n`);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
let out = `${this.clearPanel()}${msg}\n`;
|
|
1373
|
+
this.lines = 0;
|
|
1374
|
+
if (this.last) {
|
|
1375
|
+
const text = renderPanel(this.last);
|
|
1376
|
+
out += `${HIDE}${text}`;
|
|
1377
|
+
this.lines = text.split("\n").length;
|
|
1245
1378
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1379
|
+
process.stderr.write(out);
|
|
1380
|
+
}
|
|
1381
|
+
dispose() {
|
|
1382
|
+
if (this.tty) {
|
|
1383
|
+
process.stderr.write(`${SHOW}\n`);
|
|
1249
1384
|
}
|
|
1385
|
+
}
|
|
1250
1386
|
}
|
|
1251
1387
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1252
1388
|
// §9 Progress persistence (inside the store — resume from the store alone)
|
|
@@ -1257,960 +1393,1092 @@ const META_TRAINED_BYTES = "train.trainedContentBytes";
|
|
|
1257
1393
|
const META_BYTES = "train.totalBytesProcessed";
|
|
1258
1394
|
const META_CORPUS_BYTES = "train.totalCorpusBytes";
|
|
1259
1395
|
async function loadProgress(store) {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1396
|
+
try {
|
|
1397
|
+
const raw = await store.getMeta(META_COMPLETED);
|
|
1398
|
+
const deps = await store.getMeta(META_DEPOSITS);
|
|
1399
|
+
const b = await store.getMeta(META_BYTES);
|
|
1400
|
+
if (raw !== null && deps !== null && b !== null) {
|
|
1401
|
+
const completedFiles = JSON.parse(raw);
|
|
1402
|
+
if (Array.isArray(completedFiles)) {
|
|
1403
|
+
const trained = await store.getMeta(META_TRAINED_BYTES);
|
|
1404
|
+
const corpus = await store.getMeta(META_CORPUS_BYTES);
|
|
1405
|
+
return {
|
|
1406
|
+
completedFiles,
|
|
1407
|
+
depositCount: Number(deps) || 0,
|
|
1408
|
+
trainedContentBytes: Number(trained) || 0,
|
|
1409
|
+
totalBytesProcessed: Number(b) || 0,
|
|
1410
|
+
totalCorpusBytes: Number(corpus) || 0,
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1278
1413
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1414
|
+
} catch { /* corrupt/missing — start fresh */ }
|
|
1415
|
+
return {
|
|
1416
|
+
completedFiles: [],
|
|
1417
|
+
depositCount: 0,
|
|
1418
|
+
trainedContentBytes: 0,
|
|
1419
|
+
totalBytesProcessed: 0,
|
|
1420
|
+
totalCorpusBytes: 0,
|
|
1421
|
+
};
|
|
1287
1422
|
}
|
|
1288
1423
|
async function saveProgress(store, p) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1424
|
+
await store.setMeta(META_COMPLETED, JSON.stringify(p.completedFiles));
|
|
1425
|
+
await store.setMeta(META_DEPOSITS, String(p.depositCount));
|
|
1426
|
+
await store.setMeta(META_TRAINED_BYTES, String(p.trainedContentBytes));
|
|
1427
|
+
await store.setMeta(META_BYTES, String(p.totalBytesProcessed));
|
|
1428
|
+
await store.setMeta(META_CORPUS_BYTES, String(p.totalCorpusBytes));
|
|
1429
|
+
await store.setMeta("train.updatedAt", new Date().toISOString());
|
|
1430
|
+
store.commit();
|
|
1296
1431
|
}
|
|
1297
1432
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1298
1433
|
// §10 Main
|
|
1299
1434
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1300
1435
|
async function main() {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1436
|
+
// The vector indices' single memory knob (MiB) — sizes both the SQLite page
|
|
1437
|
+
// cache and the decoded-code LRU of each index. Training is HNSW-insert
|
|
1438
|
+
// bound at scale (profiled at >80% of ingest wall-clock on a 5.5M-vector
|
|
1439
|
+
// store, nearly all of it cache-missing code reads), and inserts walk the
|
|
1440
|
+
// graph, so a cache that covers the indexed codes cuts wall-clock directly
|
|
1441
|
+
// (measured ~1.6× at 1 GB vs 64 MB on a 2M-node store). 1 GB caches ~5.8M
|
|
1442
|
+
// D=1024 codes — the whole content index of a 350 MB-corpus store — and is
|
|
1443
|
+
// the default; override with VECTOR_CACHE_MB (e.g. 256 on a small-RAM
|
|
1444
|
+
// machine, 64 for the library default).
|
|
1445
|
+
const VECTOR_CACHE_MB = Math.max(0, Number(env("VECTOR_CACHE_MB", "1024")));
|
|
1446
|
+
const store = new SQliteStore({
|
|
1447
|
+
path: DB_PATH,
|
|
1448
|
+
D,
|
|
1449
|
+
vectorCacheMb: VECTOR_CACHE_MB,
|
|
1450
|
+
});
|
|
1451
|
+
// The store IS the model: memories, progress, and metadata all persist in
|
|
1452
|
+
// it, so a resumed run just reopens the same store and continues. Guard
|
|
1453
|
+
// against a changed D/SEED by comparing against what a previous run recorded.
|
|
1454
|
+
const mind = new Mind({ seed: SEED, store });
|
|
1455
|
+
// Pre-fill the vector indices' RAM caches with sequential scans (bounded by
|
|
1456
|
+
// VECTOR_CACHE_MB). A resumed run over a large store otherwise spends its
|
|
1457
|
+
// first minutes warming those caches through random point reads — the
|
|
1458
|
+
// ingest hot path is cache-miss bound until then. Seconds, once, up front.
|
|
1459
|
+
if (VECTOR_CACHE_MB > 0) {
|
|
1460
|
+
const t = Date.now();
|
|
1461
|
+
const warmed = await store.warmVectorCaches();
|
|
1462
|
+
if (warmed > 0) {
|
|
1463
|
+
process.stderr.write(
|
|
1464
|
+
` warmed vector caches: ${num(warmed)} rows in ${
|
|
1465
|
+
dur((Date.now() - t) / 1000)
|
|
1466
|
+
}\n`,
|
|
1467
|
+
);
|
|
1330
1468
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1469
|
+
}
|
|
1470
|
+
const ci = new CachedIngest(mind);
|
|
1471
|
+
const prevD = await store.getMeta("train.D");
|
|
1472
|
+
const prevSeed = await store.getMeta("train.seed");
|
|
1473
|
+
if (
|
|
1474
|
+
(prevD && Number(prevD) !== D) || (prevSeed && Number(prevSeed) !== SEED)
|
|
1475
|
+
) {
|
|
1476
|
+
process.stderr.write(
|
|
1477
|
+
`fatal: D/SEED changed (store has D=${prevD} seed=${prevSeed}, ` +
|
|
1478
|
+
`requested D=${D} seed=${SEED}). Delete ${DB_PATH}.sqlite ` +
|
|
1479
|
+
`to start fresh.\n`,
|
|
1480
|
+
);
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
await store.setMeta("train.dataset", "SmolSent+Aya+oasst2");
|
|
1484
|
+
await store.setMeta("train.D", String(D));
|
|
1485
|
+
await store.setMeta("train.seed", String(SEED));
|
|
1486
|
+
await store.setMeta("train.createdAt", new Date().toISOString());
|
|
1487
|
+
// ── counters & sampling ──
|
|
1488
|
+
let depositCount = 0;
|
|
1489
|
+
let trainedContentBytes = 0;
|
|
1490
|
+
let bytesSinceCkpt = 0;
|
|
1491
|
+
let checkpointNum = 0;
|
|
1492
|
+
let totalBytesProcessed = 0;
|
|
1493
|
+
let totalCorpusBytes = 0;
|
|
1494
|
+
const langTally = {};
|
|
1495
|
+
const t0 = Date.now();
|
|
1496
|
+
// Reservoir sample: one uniformly-random item from the current window, shown
|
|
1497
|
+
// in the recall box at each checkpoint.
|
|
1498
|
+
let sampleItem = null;
|
|
1499
|
+
let seenInWindow = 0;
|
|
1500
|
+
const sample = (it) => {
|
|
1501
|
+
seenInWindow++;
|
|
1502
|
+
if (Math.random() < 1 / seenInWindow) {
|
|
1503
|
+
sampleItem = it;
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
// ── progress panel ──
|
|
1507
|
+
const progress = new Progress();
|
|
1508
|
+
// Surface rate-limit waits from the low-level fetch retries into the live log,
|
|
1509
|
+
// so a 429 back-off reads as "waiting", never a silent hang or a dropped file.
|
|
1510
|
+
onThrottleWait = (ms, label) => {
|
|
1511
|
+
progress.log(
|
|
1512
|
+
` ${YEL}⏳${R} rate-limited (${label}); waiting ${
|
|
1513
|
+
(ms / 1000).toFixed(1)
|
|
1514
|
+
}s and retrying — not skipping`,
|
|
1515
|
+
);
|
|
1516
|
+
};
|
|
1517
|
+
const state = {
|
|
1518
|
+
exampleCount: 0,
|
|
1519
|
+
target: MAX_BYTES,
|
|
1520
|
+
elapsedS: 0,
|
|
1521
|
+
trainedBytes: 0,
|
|
1522
|
+
trainedRate: 0,
|
|
1523
|
+
bytesDone: 0,
|
|
1524
|
+
bytesTotal: 0,
|
|
1525
|
+
bytesRate: 0,
|
|
1526
|
+
fileIndex: 0,
|
|
1527
|
+
fileTotal: 0,
|
|
1528
|
+
filePath: "",
|
|
1529
|
+
fileSize: 0,
|
|
1530
|
+
fileExamples: 0,
|
|
1531
|
+
activity: "idle",
|
|
1532
|
+
dlSpeed: 0,
|
|
1533
|
+
dlDone: 0,
|
|
1534
|
+
dlTotal: 0,
|
|
1535
|
+
storeEntries: 0,
|
|
1536
|
+
cacheBytes: 0,
|
|
1537
|
+
lastSample: null,
|
|
1538
|
+
};
|
|
1539
|
+
// store.size() is async; refresh it on a slow cadence so the hot loop and
|
|
1540
|
+
// the repaint never block on a query.
|
|
1541
|
+
let cachedEntries = 0;
|
|
1542
|
+
let sizeInFlight = false;
|
|
1543
|
+
const refreshSize = () => {
|
|
1544
|
+
if (sizeInFlight) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
sizeInFlight = true;
|
|
1548
|
+
void mind.store.size()
|
|
1549
|
+
.then((n) => (cachedEntries = n))
|
|
1550
|
+
.catch(() => undefined)
|
|
1551
|
+
.finally(() => (sizeInFlight = false));
|
|
1552
|
+
};
|
|
1553
|
+
// Cache size changes only at download/delete boundaries — recompute it
|
|
1554
|
+
// lazily rather than statting the dir on every deposit.
|
|
1555
|
+
let cachedCacheBytes = 0;
|
|
1556
|
+
let lastCacheUpdate = 0;
|
|
1557
|
+
// Live download progress for the panel.
|
|
1558
|
+
let dlSlot = null;
|
|
1559
|
+
// Rolling throughput: a short EMA over wall-clock windows, so the headline
|
|
1560
|
+
// figures reflect CURRENT speed rather than a lifetime average diluted by the
|
|
1561
|
+
// listing and download phases (which train nothing).
|
|
1562
|
+
let rateT = t0;
|
|
1563
|
+
let rateTrained = 0;
|
|
1564
|
+
let rateBytes = 0;
|
|
1565
|
+
const syncState = () => {
|
|
1566
|
+
const now = Date.now();
|
|
1567
|
+
state.exampleCount = depositCount;
|
|
1568
|
+
state.trainedBytes = trainedContentBytes;
|
|
1569
|
+
state.elapsedS = (now - t0) / 1000;
|
|
1570
|
+
state.storeEntries = cachedEntries;
|
|
1571
|
+
state.bytesDone = totalBytesProcessed;
|
|
1572
|
+
state.bytesTotal = totalCorpusBytes;
|
|
1573
|
+
if (dlSlot && state.activity === "download") {
|
|
1574
|
+
state.dlDone = dlSlot.done;
|
|
1575
|
+
state.dlTotal = dlSlot.total;
|
|
1576
|
+
const ds = (now - dlSlot.t0) / 1000;
|
|
1577
|
+
state.dlSpeed = ds > 0.2 ? dlSlot.done / ds : 0;
|
|
1578
|
+
} else {
|
|
1579
|
+
state.dlDone = 0;
|
|
1580
|
+
state.dlTotal = 0;
|
|
1581
|
+
}
|
|
1582
|
+
const dt = (now - rateT) / 1000;
|
|
1583
|
+
if (dt >= 0.5) {
|
|
1584
|
+
const instTrained = (trainedContentBytes - rateTrained) / dt;
|
|
1585
|
+
const instByte = (totalBytesProcessed - rateBytes) / dt;
|
|
1586
|
+
const a = 0.3; // EMA weight on the newest sample
|
|
1587
|
+
state.trainedRate = state.trainedRate === 0
|
|
1588
|
+
? instTrained
|
|
1589
|
+
: state.trainedRate * (1 - a) + instTrained * a;
|
|
1590
|
+
state.bytesRate = state.bytesRate === 0
|
|
1591
|
+
? instByte
|
|
1592
|
+
: state.bytesRate * (1 - a) + instByte * a;
|
|
1593
|
+
rateT = now;
|
|
1594
|
+
rateTrained = trainedContentBytes;
|
|
1595
|
+
rateBytes = totalBytesProcessed;
|
|
1596
|
+
}
|
|
1597
|
+
if (now - lastCacheUpdate > 2000) {
|
|
1598
|
+
cachedCacheBytes = cacheSize();
|
|
1599
|
+
lastCacheUpdate = now;
|
|
1600
|
+
}
|
|
1601
|
+
state.cacheBytes = cachedCacheBytes;
|
|
1602
|
+
};
|
|
1603
|
+
const tick = (force = false) => {
|
|
1604
|
+
syncState();
|
|
1605
|
+
progress.render(state, force);
|
|
1606
|
+
};
|
|
1607
|
+
const paintTimer = setInterval(() => {
|
|
1608
|
+
refreshSize();
|
|
1609
|
+
tick(false);
|
|
1610
|
+
}, PROGRESS_MS);
|
|
1611
|
+
if (typeof paintTimer.unref === "function") {
|
|
1612
|
+
paintTimer.unref();
|
|
1613
|
+
}
|
|
1614
|
+
// ── keep-alive: the process must never exit on its own mid-training ──
|
|
1615
|
+
// The CPU-bound processing phase (perceive + intern + the batched vector-index
|
|
1616
|
+
// writes) hands control back to the event loop between batches via the store's
|
|
1617
|
+
// yieldToEventLoop(), which parks on an UNREF'd setImmediate so the library
|
|
1618
|
+
// never holds a process open by itself. node:sqlite is synchronous and the
|
|
1619
|
+
// vector index is in-memory, so the store's awaits resolve as microtasks with
|
|
1620
|
+
// no I/O handle, and the paint timer above is unref'd too. That leaves a window
|
|
1621
|
+
// — a batch flush that fires while we're processing an in-memory chunk, not
|
|
1622
|
+
// awaiting a disk read — in which the ONLY pending work is that unref'd
|
|
1623
|
+
// setImmediate and NOTHING is ref'd. Node's rule is to exit when only unref'd
|
|
1624
|
+
// handles remain, WITHOUT running them: the yield's continuation never fires,
|
|
1625
|
+
// main() is abandoned, and the process exits 0 silently mid-file — no error for
|
|
1626
|
+
// the fault-tolerance to catch. This one ref'd (NOT unref'd) timer guarantees a
|
|
1627
|
+
// live handle for the whole run, so the loop can never drain from under a
|
|
1628
|
+
// pending yield. Every real exit is an explicit process.exit() (finish(), the
|
|
1629
|
+
// shutdown watchdog, the second-signal path, the fatal catch), so keeping this
|
|
1630
|
+
// handle alive never delays a genuine shutdown; finish() clears it before that
|
|
1631
|
+
// final exit for tidiness. Same lesson as waitMs above (deliberately un-unref'd).
|
|
1632
|
+
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
1633
|
+
const checkpoint = () => mind.save();
|
|
1634
|
+
/** Run index maintenance: compact (remove garbage) then repair (fill gaps).
|
|
1635
|
+
* Both are idempotent — running twice produces the same result as once.
|
|
1636
|
+
* Compaction frees index space first; repair then adds back the bridges
|
|
1637
|
+
* whose gists were evicted before indexing, completing the coverage that
|
|
1638
|
+
* incremental bridge promotion alone cannot guarantee.
|
|
1639
|
+
*
|
|
1640
|
+
* Logs the number of entries removed/added so a run that silently degrades
|
|
1641
|
+
* (growing compaction count, or repair never recovering anything) is
|
|
1642
|
+
* visible in the training log. */
|
|
1643
|
+
const runIndexMaintenance = async () => {
|
|
1644
|
+
if (!INDEX_MAINTENANCE) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const removed = await mind.store.compactContentIndex();
|
|
1649
|
+
if (removed > 0) {
|
|
1650
|
+
progress.log(
|
|
1651
|
+
` ${DIM}index compact: removed ${int(removed)} isolated entries${R}`,
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
progress.log(
|
|
1656
|
+
` ${YEL}⚠ index compact failed${R}: ${
|
|
1657
|
+
err instanceof Error ? err.message : String(err)
|
|
1658
|
+
}`,
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const added = await mind.repairContentIndex();
|
|
1663
|
+
if (added > 0) {
|
|
1664
|
+
progress.log(
|
|
1665
|
+
` ${GRN}index repair: added ${int(added)} missing bridges${R}`,
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
progress.log(
|
|
1670
|
+
` ${YEL}⚠ index repair failed${R}: ${
|
|
1671
|
+
err instanceof Error ? err.message : String(err)
|
|
1672
|
+
}`,
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
// The checkpoint recall is a best-effort diagnostic. It is time-bounded so a
|
|
1677
|
+
// slow/large store can never freeze the deposit loop, and guarded so a still
|
|
1678
|
+
// running recall is never stacked on top of another.
|
|
1679
|
+
let inferBusy = false;
|
|
1680
|
+
const runRecall = async (item, n) => {
|
|
1681
|
+
if (inferBusy) {
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
inferBusy = true;
|
|
1685
|
+
try {
|
|
1686
|
+
const info = promptOf(item);
|
|
1687
|
+
const r = await withTimeout(
|
|
1688
|
+
mind.respond(info.prompt),
|
|
1689
|
+
INFER_TIMEOUT_MS,
|
|
1690
|
+
"recall",
|
|
1691
|
+
);
|
|
1692
|
+
const resp = new TextDecoder().decode(r.bytes).replace(/\u0000+/g, "");
|
|
1693
|
+
const box = renderInferenceBox(
|
|
1694
|
+
info.prompt,
|
|
1695
|
+
info.expected,
|
|
1696
|
+
resp,
|
|
1697
|
+
info.kind,
|
|
1698
|
+
n,
|
|
1699
|
+
);
|
|
1700
|
+
state.lastSample = box;
|
|
1701
|
+
if (!progress.interactive) {
|
|
1702
|
+
progress.log(box);
|
|
1703
|
+
}
|
|
1704
|
+
tick(true);
|
|
1705
|
+
} catch (err) {
|
|
1706
|
+
progress.log(
|
|
1707
|
+
` ${DIM}· checkpoint #${n} recall skipped: ${
|
|
1708
|
+
err instanceof Error ? err.message : String(err)
|
|
1709
|
+
}${R}`,
|
|
1710
|
+
);
|
|
1711
|
+
} finally {
|
|
1712
|
+
inferBusy = false;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
// ── graceful shutdown (always leaves the store consistent) ──
|
|
1716
|
+
let stopRequested = false;
|
|
1717
|
+
let stopReason = "interrupted";
|
|
1718
|
+
let finishing = false;
|
|
1719
|
+
const finish = async (why) => {
|
|
1720
|
+
if (finishing) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
finishing = true;
|
|
1724
|
+
shutdown.abort(); // unblock any straggling fetch/pipeTo
|
|
1725
|
+
tick(true);
|
|
1726
|
+
await store.setMeta("train.completedAt", new Date().toISOString());
|
|
1727
|
+
await store.setMeta("train.totalDeposits", String(depositCount));
|
|
1728
|
+
await store.setMeta("train.totalTrainedBytes", String(trainedContentBytes));
|
|
1729
|
+
await store.setMeta("train.totalBytes", String(totalBytesProcessed));
|
|
1730
|
+
await store.setMeta("train.totalCorpusBytes", String(totalCorpusBytes));
|
|
1731
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
1732
|
+
try {
|
|
1733
|
+
await runIndexMaintenance();
|
|
1734
|
+
await checkpoint();
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
process.stderr.write(
|
|
1737
|
+
`\n ${YEL}⚠ final checkpoint failed${R}: ${
|
|
1738
|
+
err instanceof Error ? err.message : String(err)
|
|
1739
|
+
}\n`,
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
clearInterval(paintTimer);
|
|
1743
|
+
clearInterval(keepAlive);
|
|
1744
|
+
progress.dispose();
|
|
1745
|
+
const elapsedS = (Date.now() - t0) / 1000;
|
|
1746
|
+
const elapsed = dur(elapsedS);
|
|
1747
|
+
const avgRate = elapsedS > 0 ? trainedContentBytes / elapsedS : 0;
|
|
1748
|
+
const tally = Object.entries(langTally)
|
|
1749
|
+
.sort((a, b) => b[1] - a[1])
|
|
1750
|
+
.map(([k, v]) => `${k}:${int(v)}`)
|
|
1751
|
+
.join(", ");
|
|
1752
|
+
let entries = depositCount;
|
|
1753
|
+
try {
|
|
1754
|
+
entries = await mind.store.size();
|
|
1755
|
+
} catch { /* best effort */ }
|
|
1756
|
+
console.log(
|
|
1757
|
+
`\n${GRN}✓${R} ${why}. ${basename(DB_PATH)}.sqlite: ` +
|
|
1758
|
+
`${int(entries)} entries, ${int(depositCount)} examples, ` +
|
|
1759
|
+
`${bytes(trainedContentBytes)} content learned ` +
|
|
1760
|
+
`${DIM}(${bytes(avgRate)}/s avg)${R}, ` +
|
|
1761
|
+
`${bytes(totalBytesProcessed)} corpus processed, ${elapsed} elapsed.` +
|
|
1762
|
+
(tally ? `\n ${DIM}per language:${R} ${tally}` : ""),
|
|
1763
|
+
);
|
|
1764
|
+
try {
|
|
1765
|
+
await store.close();
|
|
1766
|
+
} catch { /* best effort */ }
|
|
1767
|
+
process.exit(0);
|
|
1768
|
+
};
|
|
1769
|
+
const requestStop = (reason) => {
|
|
1770
|
+
if (stopRequested) {
|
|
1771
|
+
process.stderr.write(`\n${YEL}⚠ second signal — exiting now${R}\n`);
|
|
1772
|
+
process.stderr.write(SHOW);
|
|
1773
|
+
process.exit(130);
|
|
1774
|
+
}
|
|
1775
|
+
stopRequested = true;
|
|
1776
|
+
stopReason = reason;
|
|
1777
|
+
shutdown.abort();
|
|
1778
|
+
progress.log(` ${YEL}⏸${R} ${reason} — finishing current item, saving…`);
|
|
1779
|
+
const watchdog = setTimeout(() => {
|
|
1780
|
+
process.stderr.write(
|
|
1781
|
+
`\n${YEL}⚠ shutdown watchdog fired — forcing exit${R}\n`,
|
|
1782
|
+
);
|
|
1783
|
+
process.stderr.write(SHOW);
|
|
1784
|
+
process.exit(130);
|
|
1785
|
+
}, 60_000);
|
|
1786
|
+
if (typeof watchdog.unref === "function") {
|
|
1787
|
+
watchdog.unref();
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
process.on("SIGINT", () => requestStop("interrupted"));
|
|
1791
|
+
process.on("SIGTERM", () => requestStop("terminated"));
|
|
1792
|
+
// ── fail-safe: a dropped connection must never kill a long run ──
|
|
1793
|
+
process.on("unhandledRejection", (reason) => {
|
|
1794
|
+
progress.log(
|
|
1795
|
+
` ${YEL}⚠ unhandled rejection${R}: ${
|
|
1796
|
+
reason instanceof Error ? reason.message : String(reason)
|
|
1797
|
+
}`,
|
|
1798
|
+
);
|
|
1799
|
+
});
|
|
1800
|
+
process.on("uncaughtException", (err) => {
|
|
1801
|
+
const code = err?.code ?? err?.cause?.code;
|
|
1802
|
+
if (err.message === "terminated" || code === "UND_ERR_SOCKET") {
|
|
1803
|
+
progress.log(` ${YEL}⚠ connection error (ignored)${R}: ${err.message}`);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
process.stderr.write(
|
|
1807
|
+
`\n${RED}uncaught exception${R}: ${err.message}\n${err.stack ?? ""}\n`,
|
|
1808
|
+
);
|
|
1809
|
+
try {
|
|
1810
|
+
void store.setMeta("train.crashedAt", new Date().toISOString());
|
|
1811
|
+
void store.setMeta("train.crashError", err.message);
|
|
1812
|
+
void store.setMeta("train.totalDeposits", String(depositCount));
|
|
1813
|
+
} catch { /* best effort */ }
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
});
|
|
1816
|
+
// ── per-example callback: gates MAX_MB, drives checkpoints + samples ──
|
|
1817
|
+
const onDeposit = async (contentBytes) => {
|
|
1818
|
+
depositCount++;
|
|
1819
|
+
trainedContentBytes += contentBytes;
|
|
1820
|
+
bytesSinceCkpt += contentBytes;
|
|
1821
|
+
state.fileExamples++;
|
|
1822
|
+
if (bytesSinceCkpt >= CHECKPOINT_BYTES) {
|
|
1823
|
+
bytesSinceCkpt %= CHECKPOINT_BYTES;
|
|
1824
|
+
const n = ++checkpointNum;
|
|
1825
|
+
const item = sampleItem;
|
|
1826
|
+
sampleItem = null;
|
|
1827
|
+
seenInWindow = 0;
|
|
1828
|
+
if (item) {
|
|
1829
|
+
await runRecall(item, n);
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
await runIndexMaintenance();
|
|
1833
|
+
await checkpoint();
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
progress.log(
|
|
1836
|
+
` ${YEL}⚠ checkpoint failed${R}: ${
|
|
1837
|
+
err instanceof Error ? err.message : String(err)
|
|
1838
|
+
}`,
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
tick(true);
|
|
1842
|
+
} else {
|
|
1843
|
+
tick();
|
|
1844
|
+
}
|
|
1845
|
+
// Stop AFTER the deposit is counted/displayed so the final item is never
|
|
1846
|
+
// lost from the totals. A pending signal stops at this same boundary, so a
|
|
1847
|
+
// clean shutdown and a MAX_MB cap unwind through identical, tested code.
|
|
1848
|
+
return !stopRequested && trainedContentBytes < MAX_BYTES;
|
|
1849
|
+
};
|
|
1850
|
+
const cacheWarn = (m) => progress.log(` ${m}`);
|
|
1851
|
+
/** Acquire a source file: reuse a cached copy, else download `url` into the
|
|
1852
|
+
* cache under `destName` (atomic, retried, rate-limit-tolerant, shows live
|
|
1853
|
+
* byte progress). Returns the local path, or null on a non-abort failure
|
|
1854
|
+
* (logged). `label` names the file in the panel/log. */
|
|
1855
|
+
const acquire = async (url, destName, label) => {
|
|
1856
|
+
const dest = join(CACHE_DIR, destName);
|
|
1857
|
+
if (existsSync(dest)) {
|
|
1858
|
+
progress.log(` ${GRN}✓${R} ${label} ${DIM}(cached)${R}`);
|
|
1859
|
+
return dest;
|
|
1860
|
+
}
|
|
1861
|
+
let size = 0;
|
|
1862
|
+
try {
|
|
1863
|
+
size = await headSize(url);
|
|
1864
|
+
} catch { /* unknown — proceed without a cache-room reservation */ }
|
|
1865
|
+
state.activity = "download";
|
|
1866
|
+
state.filePath = label;
|
|
1867
|
+
state.fileSize = size;
|
|
1868
|
+
const slot = { done: 0, total: size, t0: Date.now() };
|
|
1869
|
+
dlSlot = slot;
|
|
1870
|
+
tick(true);
|
|
1871
|
+
try {
|
|
1872
|
+
await ensureCacheRoom(size, cacheWarn);
|
|
1873
|
+
slot.t0 = Date.now();
|
|
1874
|
+
await downloadFile(
|
|
1875
|
+
url,
|
|
1876
|
+
dest,
|
|
1877
|
+
DOWNLOAD_TRIES,
|
|
1878
|
+
(n, e) =>
|
|
1879
|
+
progress.log(
|
|
1880
|
+
` ${YEL}⚠${R} ${label} download attempt ${n}/${DOWNLOAD_TRIES}: ${e.message}`,
|
|
1881
|
+
),
|
|
1882
|
+
(done, total) => {
|
|
1883
|
+
slot.done = done;
|
|
1884
|
+
if (total > 0) {
|
|
1885
|
+
slot.total = total;
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
);
|
|
1889
|
+
} catch (e) {
|
|
1890
|
+
dlSlot = null;
|
|
1891
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
progress.log(` ${RED}✗${R} ${label} download failed: ${e.message}`);
|
|
1895
|
+
try {
|
|
1896
|
+
unlinkSync(dest);
|
|
1897
|
+
} catch { /* best effort */ }
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
dlSlot = null;
|
|
1901
|
+
const dlS = Math.max(0.001, (Date.now() - slot.t0) / 1000);
|
|
1902
|
+
const sz = statSync(dest).size;
|
|
1903
|
+
progress.log(
|
|
1904
|
+
` ${CYAN}⬇${R} ${label} ${bytes(sz)} ${DIM}${dur(dlS)} @ ${
|
|
1905
|
+
bytes(sz / dlS)
|
|
1906
|
+
}/s${R}`,
|
|
1907
|
+
);
|
|
1908
|
+
return dest;
|
|
1909
|
+
};
|
|
1910
|
+
// ── §10a SmolSent stage (the FIRST stage) ──
|
|
1911
|
+
//
|
|
1912
|
+
// Downloads each SmolSent per-pair JSONL file and streams its lines. Resume is
|
|
1913
|
+
// per-file: a fully-consumed file is recorded in completedFiles
|
|
1914
|
+
// ("smolsent::<name>"); an interrupted file is re-streamed from the top on
|
|
1915
|
+
// resume (re-deposition is idempotent). LOCAL_PATH may hold pre-downloaded
|
|
1916
|
+
// smolsent *.jsonl files.
|
|
1917
|
+
const smolToItems = (row) => {
|
|
1918
|
+
const r = toSmolSentRow(row);
|
|
1919
|
+
return r ? smolSentRowToItems(r) : null;
|
|
1920
|
+
};
|
|
1921
|
+
const trainSmolSent = async () => {
|
|
1922
|
+
if (!SMOLSENT) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
// Work-list: local *.jsonl in LOCAL_PATH, else the repo's smolsent/ files.
|
|
1929
|
+
let files;
|
|
1930
|
+
if (LOCAL_PATH) {
|
|
1931
|
+
files = readdirSync(LOCAL_PATH)
|
|
1932
|
+
.filter((f) => /\.jsonl$/i.test(f))
|
|
1933
|
+
.sort()
|
|
1934
|
+
.map((f) => ({
|
|
1935
|
+
id: `${SMOLSENT_ID}::${f}`,
|
|
1936
|
+
name: f,
|
|
1937
|
+
local: join(LOCAL_PATH, f),
|
|
1938
|
+
}));
|
|
1939
|
+
if (SMOLSENT_PAIRS.length) {
|
|
1940
|
+
const want = new Set(
|
|
1941
|
+
SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")),
|
|
1942
|
+
);
|
|
1943
|
+
files = files.filter((f) => want.has(f.name.replace(/\.jsonl$/i, "")));
|
|
1944
|
+
}
|
|
1945
|
+
} else {
|
|
1946
|
+
let paths;
|
|
1947
|
+
try {
|
|
1948
|
+
paths = await listSmolSentFiles();
|
|
1949
|
+
} catch (e) {
|
|
1950
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
progress.log(
|
|
1954
|
+
` ${RED}✗${R} SmolSent file listing failed: ${e.message}`,
|
|
1955
|
+
);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
files = paths.map((path) => ({
|
|
1959
|
+
id: `${SMOLSENT_ID}::${basename(path)}`,
|
|
1960
|
+
name: basename(path),
|
|
1961
|
+
// owner/name and the file path are URL PATH segments — do not encode "/".
|
|
1962
|
+
url:
|
|
1963
|
+
`https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
|
|
1964
|
+
}));
|
|
1965
|
+
}
|
|
1966
|
+
if (files.length === 0) {
|
|
1967
|
+
progress.log(` ${DIM}· no SmolSent files found — skipping${R}`);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const p = await loadProgress(store);
|
|
1971
|
+
const done = new Set(p.completedFiles);
|
|
1972
|
+
const remaining = files.filter((f) => !done.has(f.id));
|
|
1973
|
+
if (remaining.length === 0) {
|
|
1974
|
+
progress.log(` ${DIM}· SmolSent already trained — skipping${R}`);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
state.fileTotal = files.length;
|
|
1978
|
+
progress.log(
|
|
1979
|
+
` ${GRN}✓${R} SmolSent: ${remaining.length}/${files.length} translation file(s) to train`,
|
|
1980
|
+
);
|
|
1981
|
+
let idx = 0;
|
|
1982
|
+
for (const f of files) {
|
|
1983
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
1984
|
+
break;
|
|
1985
|
+
}
|
|
1986
|
+
idx++;
|
|
1987
|
+
if (done.has(f.id)) {
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
// Acquire (download or reuse), then stream the JSONL.
|
|
1991
|
+
let path = f.local ?? "";
|
|
1992
|
+
let downloaded = false;
|
|
1993
|
+
if (!path) {
|
|
1994
|
+
const got = await acquire(
|
|
1995
|
+
f.url,
|
|
1996
|
+
f.id.replace(/[^A-Za-z0-9._-]+/g, "_"),
|
|
1997
|
+
`SmolSent ${f.name}`,
|
|
1998
|
+
);
|
|
1999
|
+
if (!got) {
|
|
2000
|
+
if (stopRequested) {
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
continue; // a single failed file never aborts the stage
|
|
2004
|
+
}
|
|
2005
|
+
path = got;
|
|
2006
|
+
downloaded = true;
|
|
2007
|
+
}
|
|
2008
|
+
// Accumulate known corpus bytes so the progress bar shows a
|
|
2009
|
+
// meaningful ETA — grows as each file's size is discovered.
|
|
2010
|
+
try {
|
|
2011
|
+
totalCorpusBytes += statSync(path).size;
|
|
2012
|
+
} catch { /* best effort */ }
|
|
2013
|
+
state.activity = "process";
|
|
2014
|
+
state.fileIndex = idx;
|
|
2015
|
+
state.filePath = `SmolSent ${f.name}`;
|
|
2016
|
+
state.fileExamples = 0;
|
|
2017
|
+
tick(true);
|
|
2018
|
+
const p0 = Date.now();
|
|
2019
|
+
let res;
|
|
2020
|
+
try {
|
|
2021
|
+
res = await processJsonl(
|
|
2022
|
+
path,
|
|
2023
|
+
smolToItems,
|
|
2024
|
+
ci,
|
|
2025
|
+
onDeposit,
|
|
2026
|
+
sample,
|
|
2027
|
+
MAX_SMOLSENT_CHARS * 4,
|
|
2028
|
+
);
|
|
2029
|
+
} catch (e) {
|
|
2030
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
progress.log(
|
|
2034
|
+
` ${RED}✗${R} SmolSent ${f.name} parse failed: ${e.message}`,
|
|
2035
|
+
);
|
|
2036
|
+
if (downloaded) {
|
|
2037
|
+
try {
|
|
2038
|
+
unlinkSync(path);
|
|
2039
|
+
} catch { /* best effort */ }
|
|
2040
|
+
}
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
langTally["smolsent"] = (langTally["smolsent"] ?? 0) + res.examples;
|
|
2044
|
+
progress.log(
|
|
2045
|
+
` ${GRN}✓${R} ${
|
|
2046
|
+
f.name.replace(/\.jsonl$/i, "")
|
|
2047
|
+
} ${DIM}[translation]${R} → ${int(res.examples)} facts ${DIM}in ${
|
|
2048
|
+
dur((Date.now() - p0) / 1000)
|
|
2049
|
+
}${R}` +
|
|
2050
|
+
(res.skipped
|
|
2051
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2052
|
+
: "") +
|
|
2053
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
2054
|
+
);
|
|
2055
|
+
if (!res.stopped) {
|
|
1524
2056
|
try {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
let stopReason = "interrupted";
|
|
1544
|
-
let finishing = false;
|
|
1545
|
-
const finish = async (why) => {
|
|
1546
|
-
if (finishing)
|
|
1547
|
-
return;
|
|
1548
|
-
finishing = true;
|
|
1549
|
-
shutdown.abort(); // unblock any straggling fetch/pipeTo
|
|
1550
|
-
tick(true);
|
|
1551
|
-
await store.setMeta("train.completedAt", new Date().toISOString());
|
|
1552
|
-
await store.setMeta("train.totalDeposits", String(depositCount));
|
|
1553
|
-
await store.setMeta("train.totalTrainedBytes", String(trainedContentBytes));
|
|
1554
|
-
await store.setMeta("train.totalBytes", String(totalBytesProcessed));
|
|
1555
|
-
await store.setMeta("train.totalCorpusBytes", String(totalCorpusBytes));
|
|
2057
|
+
totalBytesProcessed += statSync(path).size;
|
|
2058
|
+
} catch { /* best effort */ }
|
|
2059
|
+
if (downloaded) {
|
|
2060
|
+
try {
|
|
2061
|
+
unlinkSync(path);
|
|
2062
|
+
} catch { /* best effort */ }
|
|
2063
|
+
}
|
|
2064
|
+
done.add(f.id);
|
|
2065
|
+
p.completedFiles.push(f.id);
|
|
2066
|
+
}
|
|
2067
|
+
try {
|
|
2068
|
+
await saveProgress(store, {
|
|
2069
|
+
completedFiles: p.completedFiles,
|
|
2070
|
+
depositCount,
|
|
2071
|
+
trainedContentBytes,
|
|
2072
|
+
totalBytesProcessed,
|
|
2073
|
+
totalCorpusBytes,
|
|
2074
|
+
});
|
|
1556
2075
|
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2076
|
+
} catch { /* best effort — finish() will retry */ }
|
|
2077
|
+
if (res.stopped) {
|
|
2078
|
+
break; // cap/signal — leave file un-completed for resume
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
// ── §10b Aya Dataset stage (runs AFTER SmolSent) ──
|
|
2083
|
+
//
|
|
2084
|
+
// Downloads the one train Parquet file and reads it row-group by row-group
|
|
2085
|
+
// (hyparquet + Snappy) — one (inputs → targets) fact per row. Marked complete
|
|
2086
|
+
// only when fully consumed; an interrupted run re-reads from the top on resume
|
|
2087
|
+
// (re-deposition is idempotent). LOCAL_PATH may hold a pre-downloaded *.parquet.
|
|
2088
|
+
const ayaToItems = (row) => {
|
|
2089
|
+
const r = toAyaRow(row);
|
|
2090
|
+
return r ? ayaRowToItems(r) : null;
|
|
2091
|
+
};
|
|
2092
|
+
const trainAya = async () => {
|
|
2093
|
+
if (!AYA) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const p = await loadProgress(store);
|
|
2100
|
+
if (p.completedFiles.includes(AYA_ID)) {
|
|
2101
|
+
progress.log(` ${DIM}· Aya Dataset already trained — skipping${R}`);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
let path = "", downloaded = false;
|
|
2105
|
+
if (LOCAL_PATH) {
|
|
2106
|
+
const hit = readdirSync(LOCAL_PATH).find((f) =>
|
|
2107
|
+
/aya.*\.parquet$/i.test(f) || /\.parquet$/i.test(f)
|
|
2108
|
+
);
|
|
2109
|
+
if (!hit) {
|
|
2110
|
+
progress.log(
|
|
2111
|
+
` ${DIM}· no Aya *.parquet in ${LOCAL_PATH} — skipping${R}`,
|
|
2112
|
+
);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
path = join(LOCAL_PATH, hit);
|
|
2116
|
+
} else {
|
|
2117
|
+
const got = await acquire(AYA_URL, "aya_train.parquet", "Aya Dataset");
|
|
2118
|
+
if (!got) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
path = got;
|
|
2122
|
+
downloaded = true;
|
|
2123
|
+
}
|
|
2124
|
+
try {
|
|
2125
|
+
totalCorpusBytes += statSync(path).size;
|
|
2126
|
+
} catch { /* best effort */ }
|
|
2127
|
+
state.fileTotal = 1;
|
|
2128
|
+
state.fileIndex = 1;
|
|
2129
|
+
state.activity = "process";
|
|
2130
|
+
state.filePath = "Aya Dataset";
|
|
2131
|
+
state.fileExamples = 0;
|
|
2132
|
+
tick(true);
|
|
2133
|
+
const p0 = Date.now();
|
|
2134
|
+
let res;
|
|
2135
|
+
try {
|
|
2136
|
+
res = await processParquet(path, ayaToItems, ci, onDeposit, sample);
|
|
2137
|
+
} catch (e) {
|
|
2138
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
progress.log(` ${RED}✗${R} Aya processing failed: ${e.message}`);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
langTally["aya"] = (langTally["aya"] ?? 0) + res.examples;
|
|
2145
|
+
progress.log(
|
|
2146
|
+
` ${GRN}✓${R} Aya Dataset ${DIM}[multilingual chat]${R} → ${
|
|
2147
|
+
int(res.examples)
|
|
2148
|
+
} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2149
|
+
(res.skipped
|
|
2150
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2151
|
+
: "") +
|
|
2152
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
2153
|
+
);
|
|
2154
|
+
if (!res.stopped) {
|
|
2155
|
+
try {
|
|
2156
|
+
totalBytesProcessed += statSync(path).size;
|
|
2157
|
+
} catch { /* best effort */ }
|
|
2158
|
+
if (downloaded) {
|
|
1557
2159
|
try {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
progress.log(
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
process.on("unhandledRejection", (reason) => {
|
|
1613
|
-
progress.log(` ${YEL}⚠ unhandled rejection${R}: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
1614
|
-
});
|
|
1615
|
-
process.on("uncaughtException", (err) => {
|
|
1616
|
-
const code = err?.code ?? err?.cause?.code;
|
|
1617
|
-
if (err.message === "terminated" || code === "UND_ERR_SOCKET") {
|
|
1618
|
-
progress.log(` ${YEL}⚠ connection error (ignored)${R}: ${err.message}`);
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
process.stderr.write(`\n${RED}uncaught exception${R}: ${err.message}\n${err.stack ?? ""}\n`);
|
|
1622
|
-
try {
|
|
1623
|
-
void store.setMeta("train.crashedAt", new Date().toISOString());
|
|
1624
|
-
void store.setMeta("train.crashError", err.message);
|
|
1625
|
-
void store.setMeta("train.totalDeposits", String(depositCount));
|
|
1626
|
-
}
|
|
1627
|
-
catch { /* best effort */ }
|
|
1628
|
-
process.exit(1);
|
|
1629
|
-
});
|
|
1630
|
-
// ── per-example callback: gates MAX_MB, drives checkpoints + samples ──
|
|
1631
|
-
const onDeposit = async (contentBytes) => {
|
|
1632
|
-
depositCount++;
|
|
1633
|
-
trainedContentBytes += contentBytes;
|
|
1634
|
-
bytesSinceCkpt += contentBytes;
|
|
1635
|
-
state.fileExamples++;
|
|
1636
|
-
if (bytesSinceCkpt >= CHECKPOINT_BYTES) {
|
|
1637
|
-
bytesSinceCkpt %= CHECKPOINT_BYTES;
|
|
1638
|
-
const n = ++checkpointNum;
|
|
1639
|
-
const item = sampleItem;
|
|
1640
|
-
sampleItem = null;
|
|
1641
|
-
seenInWindow = 0;
|
|
1642
|
-
if (item)
|
|
1643
|
-
await runRecall(item, n);
|
|
1644
|
-
try {
|
|
1645
|
-
await runIndexMaintenance();
|
|
1646
|
-
await checkpoint();
|
|
1647
|
-
}
|
|
1648
|
-
catch (err) {
|
|
1649
|
-
progress.log(` ${YEL}⚠ checkpoint failed${R}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1650
|
-
}
|
|
1651
|
-
tick(true);
|
|
1652
|
-
}
|
|
1653
|
-
else {
|
|
1654
|
-
tick();
|
|
1655
|
-
}
|
|
1656
|
-
// Stop AFTER the deposit is counted/displayed so the final item is never
|
|
1657
|
-
// lost from the totals. A pending signal stops at this same boundary, so a
|
|
1658
|
-
// clean shutdown and a MAX_MB cap unwind through identical, tested code.
|
|
1659
|
-
return !stopRequested && trainedContentBytes < MAX_BYTES;
|
|
1660
|
-
};
|
|
1661
|
-
const cacheWarn = (m) => progress.log(` ${m}`);
|
|
1662
|
-
/** Acquire a source file: reuse a cached copy, else download `url` into the
|
|
1663
|
-
* cache under `destName` (atomic, retried, rate-limit-tolerant, shows live
|
|
1664
|
-
* byte progress). Returns the local path, or null on a non-abort failure
|
|
1665
|
-
* (logged). `label` names the file in the panel/log. */
|
|
1666
|
-
const acquire = async (url, destName, label) => {
|
|
1667
|
-
const dest = join(CACHE_DIR, destName);
|
|
1668
|
-
if (existsSync(dest)) {
|
|
1669
|
-
progress.log(` ${GRN}✓${R} ${label} ${DIM}(cached)${R}`);
|
|
1670
|
-
return dest;
|
|
1671
|
-
}
|
|
2160
|
+
unlinkSync(path);
|
|
2161
|
+
} catch { /* best effort */ }
|
|
2162
|
+
}
|
|
2163
|
+
p.completedFiles.push(AYA_ID);
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
await saveProgress(store, {
|
|
2167
|
+
completedFiles: p.completedFiles,
|
|
2168
|
+
depositCount,
|
|
2169
|
+
trainedContentBytes,
|
|
2170
|
+
totalBytesProcessed,
|
|
2171
|
+
totalCorpusBytes,
|
|
2172
|
+
});
|
|
2173
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2174
|
+
} catch { /* best effort — finish() will retry */ }
|
|
2175
|
+
};
|
|
2176
|
+
// ── §10c oasst2 stage (multi-turn conversations; runs AFTER Aya; both modes) ──
|
|
2177
|
+
//
|
|
2178
|
+
// Resolves the source (a local *trees*.jsonl.gz in
|
|
2179
|
+
// LOCAL_PATH, else the gzip downloaded to the cache), streams it, and marks
|
|
2180
|
+
// OASST_ID complete only when fully consumed (an interrupted run re-streams
|
|
2181
|
+
// from the top; re-deposition is idempotent). Only multi-turn conversations
|
|
2182
|
+
// are deposited — single Q→A trees are skipped inside processOasst.
|
|
2183
|
+
const trainOasst = async () => {
|
|
2184
|
+
if (!OASST) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const p = await loadProgress(store);
|
|
2191
|
+
if (p.completedFiles.includes(OASST_ID)) {
|
|
2192
|
+
progress.log(` ${DIM}· oasst2 already trained — skipping${R}`);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
let gzPath = "";
|
|
2196
|
+
let downloaded = false;
|
|
2197
|
+
if (LOCAL_PATH) {
|
|
2198
|
+
const hit = readdirSync(LOCAL_PATH).find((f) =>
|
|
2199
|
+
/oasst.*trees.*\.jsonl\.gz$/i.test(f) || /oasst.*\.jsonl\.gz$/i.test(f)
|
|
2200
|
+
);
|
|
2201
|
+
if (!hit) {
|
|
2202
|
+
progress.log(
|
|
2203
|
+
` ${DIM}· no oasst2 *trees*.jsonl.gz in ${LOCAL_PATH} — skipping${R}`,
|
|
2204
|
+
);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
gzPath = join(LOCAL_PATH, hit);
|
|
2208
|
+
} else {
|
|
2209
|
+
const dest = join(CACHE_DIR, "oasst2_ready.trees.jsonl.gz");
|
|
2210
|
+
if (existsSync(dest)) {
|
|
2211
|
+
gzPath = dest; // reuse a copy left by a previous interrupted run
|
|
2212
|
+
progress.log(` ${GRN}✓${R} oasst2 trees ${DIM}(cached)${R}`);
|
|
2213
|
+
} else {
|
|
1672
2214
|
let size = 0;
|
|
1673
2215
|
try {
|
|
1674
|
-
|
|
1675
|
-
}
|
|
1676
|
-
catch { /* unknown — proceed without a cache-room reservation */ }
|
|
2216
|
+
size = await headSize(OASST_URL);
|
|
2217
|
+
} catch { /* unknown — proceed without a cache-room reservation */ }
|
|
1677
2218
|
state.activity = "download";
|
|
1678
|
-
state.filePath =
|
|
2219
|
+
state.filePath = "oasst2 trees";
|
|
1679
2220
|
state.fileSize = size;
|
|
1680
2221
|
const slot = { done: 0, total: size, t0: Date.now() };
|
|
1681
2222
|
dlSlot = slot;
|
|
1682
2223
|
tick(true);
|
|
1683
2224
|
try {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
|
|
2225
|
+
await ensureCacheRoom(size, cacheWarn);
|
|
2226
|
+
slot.t0 = Date.now();
|
|
2227
|
+
await downloadFile(
|
|
2228
|
+
OASST_URL,
|
|
2229
|
+
dest,
|
|
2230
|
+
DOWNLOAD_TRIES,
|
|
2231
|
+
(n, e) =>
|
|
2232
|
+
progress.log(
|
|
2233
|
+
` ${YEL}⚠${R} oasst2 download attempt ${n}/${DOWNLOAD_TRIES}: ${e.message}`,
|
|
2234
|
+
),
|
|
2235
|
+
(done, total) => {
|
|
2236
|
+
slot.done = done;
|
|
2237
|
+
if (total > 0) {
|
|
2238
|
+
slot.total = total;
|
|
2239
|
+
}
|
|
2240
|
+
},
|
|
2241
|
+
);
|
|
2242
|
+
} catch (e) {
|
|
2243
|
+
dlSlot = null;
|
|
2244
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
progress.log(` ${RED}✗${R} oasst2 download failed: ${e.message}`);
|
|
2248
|
+
try {
|
|
2249
|
+
unlinkSync(dest);
|
|
2250
|
+
} catch { /* best effort */ }
|
|
2251
|
+
return;
|
|
1702
2252
|
}
|
|
1703
2253
|
dlSlot = null;
|
|
1704
2254
|
const dlS = Math.max(0.001, (Date.now() - slot.t0) / 1000);
|
|
1705
2255
|
const sz = statSync(dest).size;
|
|
1706
|
-
progress.log(
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
name: basename(path),
|
|
1755
|
-
// owner/name and the file path are URL PATH segments — do not encode "/".
|
|
1756
|
-
url: `https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
|
|
1757
|
-
}));
|
|
1758
|
-
}
|
|
1759
|
-
if (files.length === 0) {
|
|
1760
|
-
progress.log(` ${DIM}· no SmolSent files found — skipping${R}`);
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
const p = await loadProgress(store);
|
|
1764
|
-
const done = new Set(p.completedFiles);
|
|
1765
|
-
const remaining = files.filter((f) => !done.has(f.id));
|
|
1766
|
-
if (remaining.length === 0) {
|
|
1767
|
-
progress.log(` ${DIM}· SmolSent already trained — skipping${R}`);
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
state.fileTotal = files.length;
|
|
1771
|
-
progress.log(` ${GRN}✓${R} SmolSent: ${remaining.length}/${files.length} translation file(s) to train`);
|
|
1772
|
-
let idx = 0;
|
|
1773
|
-
for (const f of files) {
|
|
1774
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1775
|
-
break;
|
|
1776
|
-
idx++;
|
|
1777
|
-
if (done.has(f.id))
|
|
1778
|
-
continue;
|
|
1779
|
-
// Acquire (download or reuse), then stream the JSONL.
|
|
1780
|
-
let path = f.local ?? "";
|
|
1781
|
-
let downloaded = false;
|
|
1782
|
-
if (!path) {
|
|
1783
|
-
const got = await acquire(f.url, f.id.replace(/[^A-Za-z0-9._-]+/g, "_"), `SmolSent ${f.name}`);
|
|
1784
|
-
if (!got) {
|
|
1785
|
-
if (stopRequested)
|
|
1786
|
-
break;
|
|
1787
|
-
continue; // a single failed file never aborts the stage
|
|
1788
|
-
}
|
|
1789
|
-
path = got;
|
|
1790
|
-
downloaded = true;
|
|
1791
|
-
}
|
|
1792
|
-
// Accumulate known corpus bytes so the progress bar shows a
|
|
1793
|
-
// meaningful ETA — grows as each file's size is discovered.
|
|
1794
|
-
try {
|
|
1795
|
-
totalCorpusBytes += statSync(path).size;
|
|
1796
|
-
}
|
|
1797
|
-
catch { /* best effort */ }
|
|
1798
|
-
state.activity = "process";
|
|
1799
|
-
state.fileIndex = idx;
|
|
1800
|
-
state.filePath = `SmolSent ${f.name}`;
|
|
1801
|
-
state.fileExamples = 0;
|
|
1802
|
-
tick(true);
|
|
1803
|
-
const p0 = Date.now();
|
|
1804
|
-
let res;
|
|
1805
|
-
try {
|
|
1806
|
-
res = await processJsonl(path, smolToItems, ci, onDeposit, sample, MAX_SMOLSENT_CHARS * 4);
|
|
1807
|
-
}
|
|
1808
|
-
catch (e) {
|
|
1809
|
-
if (stopRequested || e?.name === "AbortError")
|
|
1810
|
-
break;
|
|
1811
|
-
progress.log(` ${RED}✗${R} SmolSent ${f.name} parse failed: ${e.message}`);
|
|
1812
|
-
if (downloaded) {
|
|
1813
|
-
try {
|
|
1814
|
-
unlinkSync(path);
|
|
1815
|
-
}
|
|
1816
|
-
catch { /* best effort */ }
|
|
1817
|
-
}
|
|
1818
|
-
continue;
|
|
1819
|
-
}
|
|
1820
|
-
langTally["smolsent"] = (langTally["smolsent"] ?? 0) + res.examples;
|
|
1821
|
-
progress.log(` ${GRN}✓${R} ${f.name.replace(/\.jsonl$/i, "")} ${DIM}[translation]${R} → ${int(res.examples)} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
1822
|
-
(res.skipped
|
|
1823
|
-
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
1824
|
-
: "") +
|
|
1825
|
-
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
1826
|
-
if (!res.stopped) {
|
|
1827
|
-
try {
|
|
1828
|
-
totalBytesProcessed += statSync(path).size;
|
|
1829
|
-
}
|
|
1830
|
-
catch { /* best effort */ }
|
|
1831
|
-
if (downloaded) {
|
|
1832
|
-
try {
|
|
1833
|
-
unlinkSync(path);
|
|
1834
|
-
}
|
|
1835
|
-
catch { /* best effort */ }
|
|
1836
|
-
}
|
|
1837
|
-
done.add(f.id);
|
|
1838
|
-
p.completedFiles.push(f.id);
|
|
1839
|
-
}
|
|
1840
|
-
try {
|
|
1841
|
-
await saveProgress(store, {
|
|
1842
|
-
completedFiles: p.completedFiles,
|
|
1843
|
-
depositCount,
|
|
1844
|
-
trainedContentBytes,
|
|
1845
|
-
totalBytesProcessed,
|
|
1846
|
-
totalCorpusBytes,
|
|
1847
|
-
});
|
|
1848
|
-
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
1849
|
-
}
|
|
1850
|
-
catch { /* best effort — finish() will retry */ }
|
|
1851
|
-
if (res.stopped)
|
|
1852
|
-
break; // cap/signal — leave file un-completed for resume
|
|
1853
|
-
}
|
|
1854
|
-
};
|
|
1855
|
-
// ── §10b Aya Dataset stage (runs AFTER SmolSent) ──
|
|
1856
|
-
//
|
|
1857
|
-
// Downloads the one train Parquet file and reads it row-group by row-group
|
|
1858
|
-
// (hyparquet + Snappy) — one (inputs → targets) fact per row. Marked complete
|
|
1859
|
-
// only when fully consumed; an interrupted run re-reads from the top on resume
|
|
1860
|
-
// (re-deposition is idempotent). LOCAL_PATH may hold a pre-downloaded *.parquet.
|
|
1861
|
-
const ayaToItems = (row) => {
|
|
1862
|
-
const r = toAyaRow(row);
|
|
1863
|
-
return r ? ayaRowToItems(r) : null;
|
|
1864
|
-
};
|
|
1865
|
-
const trainAya = async () => {
|
|
1866
|
-
if (!AYA)
|
|
1867
|
-
return;
|
|
1868
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1869
|
-
return;
|
|
1870
|
-
const p = await loadProgress(store);
|
|
1871
|
-
if (p.completedFiles.includes(AYA_ID)) {
|
|
1872
|
-
progress.log(` ${DIM}· Aya Dataset already trained — skipping${R}`);
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
let path = "", downloaded = false;
|
|
1876
|
-
if (LOCAL_PATH) {
|
|
1877
|
-
const hit = readdirSync(LOCAL_PATH).find((f) => /aya.*\.parquet$/i.test(f) || /\.parquet$/i.test(f));
|
|
1878
|
-
if (!hit) {
|
|
1879
|
-
progress.log(` ${DIM}· no Aya *.parquet in ${LOCAL_PATH} — skipping${R}`);
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
path = join(LOCAL_PATH, hit);
|
|
1883
|
-
}
|
|
1884
|
-
else {
|
|
1885
|
-
const got = await acquire(AYA_URL, "aya_train.parquet", "Aya Dataset");
|
|
1886
|
-
if (!got)
|
|
1887
|
-
return;
|
|
1888
|
-
path = got;
|
|
1889
|
-
downloaded = true;
|
|
1890
|
-
}
|
|
1891
|
-
try {
|
|
1892
|
-
totalCorpusBytes += statSync(path).size;
|
|
1893
|
-
}
|
|
1894
|
-
catch { /* best effort */ }
|
|
1895
|
-
state.fileTotal = 1;
|
|
1896
|
-
state.fileIndex = 1;
|
|
1897
|
-
state.activity = "process";
|
|
1898
|
-
state.filePath = "Aya Dataset";
|
|
1899
|
-
state.fileExamples = 0;
|
|
1900
|
-
tick(true);
|
|
1901
|
-
const p0 = Date.now();
|
|
1902
|
-
let res;
|
|
1903
|
-
try {
|
|
1904
|
-
res = await processParquet(path, ayaToItems, ci, onDeposit, sample);
|
|
1905
|
-
}
|
|
1906
|
-
catch (e) {
|
|
1907
|
-
if (stopRequested || e?.name === "AbortError")
|
|
1908
|
-
return;
|
|
1909
|
-
progress.log(` ${RED}✗${R} Aya processing failed: ${e.message}`);
|
|
1910
|
-
return;
|
|
1911
|
-
}
|
|
1912
|
-
langTally["aya"] = (langTally["aya"] ?? 0) + res.examples;
|
|
1913
|
-
progress.log(` ${GRN}✓${R} Aya Dataset ${DIM}[multilingual chat]${R} → ${int(res.examples)} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
1914
|
-
(res.skipped
|
|
1915
|
-
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
1916
|
-
: "") +
|
|
1917
|
-
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
1918
|
-
if (!res.stopped) {
|
|
1919
|
-
try {
|
|
1920
|
-
totalBytesProcessed += statSync(path).size;
|
|
1921
|
-
}
|
|
1922
|
-
catch { /* best effort */ }
|
|
1923
|
-
if (downloaded) {
|
|
1924
|
-
try {
|
|
1925
|
-
unlinkSync(path);
|
|
1926
|
-
}
|
|
1927
|
-
catch { /* best effort */ }
|
|
1928
|
-
}
|
|
1929
|
-
p.completedFiles.push(AYA_ID);
|
|
1930
|
-
}
|
|
1931
|
-
try {
|
|
1932
|
-
await saveProgress(store, {
|
|
1933
|
-
completedFiles: p.completedFiles,
|
|
1934
|
-
depositCount,
|
|
1935
|
-
trainedContentBytes,
|
|
1936
|
-
totalBytesProcessed,
|
|
1937
|
-
totalCorpusBytes,
|
|
1938
|
-
});
|
|
1939
|
-
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
1940
|
-
}
|
|
1941
|
-
catch { /* best effort — finish() will retry */ }
|
|
1942
|
-
};
|
|
1943
|
-
// ── §10c oasst2 stage (multi-turn conversations; runs AFTER Aya; both modes) ──
|
|
1944
|
-
//
|
|
1945
|
-
// Resolves the source (a local *trees*.jsonl.gz in
|
|
1946
|
-
// LOCAL_PATH, else the gzip downloaded to the cache), streams it, and marks
|
|
1947
|
-
// OASST_ID complete only when fully consumed (an interrupted run re-streams
|
|
1948
|
-
// from the top; re-deposition is idempotent). Only multi-turn conversations
|
|
1949
|
-
// are deposited — single Q→A trees are skipped inside processOasst.
|
|
1950
|
-
const trainOasst = async () => {
|
|
1951
|
-
if (!OASST)
|
|
1952
|
-
return;
|
|
1953
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
1954
|
-
return;
|
|
1955
|
-
const p = await loadProgress(store);
|
|
1956
|
-
if (p.completedFiles.includes(OASST_ID)) {
|
|
1957
|
-
progress.log(` ${DIM}· oasst2 already trained — skipping${R}`);
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
let gzPath = "";
|
|
1961
|
-
let downloaded = false;
|
|
1962
|
-
if (LOCAL_PATH) {
|
|
1963
|
-
const hit = readdirSync(LOCAL_PATH).find((f) => /oasst.*trees.*\.jsonl\.gz$/i.test(f) || /oasst.*\.jsonl\.gz$/i.test(f));
|
|
1964
|
-
if (!hit) {
|
|
1965
|
-
progress.log(` ${DIM}· no oasst2 *trees*.jsonl.gz in ${LOCAL_PATH} — skipping${R}`);
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
gzPath = join(LOCAL_PATH, hit);
|
|
1969
|
-
}
|
|
1970
|
-
else {
|
|
1971
|
-
const dest = join(CACHE_DIR, "oasst2_ready.trees.jsonl.gz");
|
|
1972
|
-
if (existsSync(dest)) {
|
|
1973
|
-
gzPath = dest; // reuse a copy left by a previous interrupted run
|
|
1974
|
-
progress.log(` ${GRN}✓${R} oasst2 trees ${DIM}(cached)${R}`);
|
|
1975
|
-
}
|
|
1976
|
-
else {
|
|
1977
|
-
let size = 0;
|
|
1978
|
-
try {
|
|
1979
|
-
size = await headSize(OASST_URL);
|
|
1980
|
-
}
|
|
1981
|
-
catch { /* unknown — proceed without a cache-room reservation */ }
|
|
1982
|
-
state.activity = "download";
|
|
1983
|
-
state.filePath = "oasst2 trees";
|
|
1984
|
-
state.fileSize = size;
|
|
1985
|
-
const slot = { done: 0, total: size, t0: Date.now() };
|
|
1986
|
-
dlSlot = slot;
|
|
1987
|
-
tick(true);
|
|
1988
|
-
try {
|
|
1989
|
-
await ensureCacheRoom(size, cacheWarn);
|
|
1990
|
-
slot.t0 = Date.now();
|
|
1991
|
-
await downloadFile(OASST_URL, dest, DOWNLOAD_TRIES, (n, e) => progress.log(` ${YEL}⚠${R} oasst2 download attempt ${n}/${DOWNLOAD_TRIES}: ${e.message}`), (done, total) => {
|
|
1992
|
-
slot.done = done;
|
|
1993
|
-
if (total > 0)
|
|
1994
|
-
slot.total = total;
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
catch (e) {
|
|
1998
|
-
dlSlot = null;
|
|
1999
|
-
if (stopRequested || e?.name === "AbortError")
|
|
2000
|
-
return;
|
|
2001
|
-
progress.log(` ${RED}✗${R} oasst2 download failed: ${e.message}`);
|
|
2002
|
-
try {
|
|
2003
|
-
unlinkSync(dest);
|
|
2004
|
-
}
|
|
2005
|
-
catch { /* best effort */ }
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
dlSlot = null;
|
|
2009
|
-
const dlS = Math.max(0.001, (Date.now() - slot.t0) / 1000);
|
|
2010
|
-
const sz = statSync(dest).size;
|
|
2011
|
-
progress.log(` ${CYAN}⬇${R} oasst2 trees ${bytes(sz)} ` +
|
|
2012
|
-
`${DIM}${dur(dlS)} @ ${bytes(sz / dlS)}/s${R}`);
|
|
2013
|
-
gzPath = dest;
|
|
2014
|
-
downloaded = true;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
try {
|
|
2018
|
-
totalCorpusBytes += statSync(gzPath).size;
|
|
2019
|
-
}
|
|
2020
|
-
catch { /* best effort */ }
|
|
2021
|
-
state.fileTotal = 1;
|
|
2022
|
-
state.fileIndex = 1;
|
|
2023
|
-
// Stream the trees.
|
|
2024
|
-
state.activity = "process";
|
|
2025
|
-
state.filePath = "oasst2 (multi-turn)";
|
|
2026
|
-
state.fileExamples = 0;
|
|
2027
|
-
tick(true);
|
|
2028
|
-
const p0 = Date.now();
|
|
2029
|
-
let result;
|
|
2030
|
-
try {
|
|
2031
|
-
result = await processOasst(gzPath, ci, onDeposit, sample);
|
|
2032
|
-
}
|
|
2033
|
-
catch (e) {
|
|
2034
|
-
if (stopRequested || e?.name === "AbortError")
|
|
2035
|
-
return;
|
|
2036
|
-
progress.log(` ${RED}✗${R} oasst2 processing failed: ${e.message}`);
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
const { examples, stopped, skipped, multi } = result;
|
|
2040
|
-
langTally["oasst2"] = (langTally["oasst2"] ?? 0) + examples;
|
|
2041
|
-
progress.log(` ${GRN}✓${R} oasst2 ${DIM}[multi-turn chat]${R} → ${int(examples)} examples from ${int(multi)} conversation(s) ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2042
|
-
(skipped
|
|
2043
|
-
? ` ${YEL}· ${int(skipped)} malformed line(s) skipped${R}`
|
|
2044
|
-
: "") +
|
|
2045
|
-
(stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
2046
|
-
// Only mark complete (and reclaim the cache) when fully consumed.
|
|
2047
|
-
if (!stopped) {
|
|
2048
|
-
try {
|
|
2049
|
-
totalBytesProcessed += statSync(gzPath).size;
|
|
2050
|
-
}
|
|
2051
|
-
catch { /* best effort */ }
|
|
2052
|
-
if (downloaded) {
|
|
2053
|
-
try {
|
|
2054
|
-
unlinkSync(gzPath);
|
|
2055
|
-
}
|
|
2056
|
-
catch { /* best effort */ }
|
|
2057
|
-
}
|
|
2058
|
-
p.completedFiles.push(OASST_ID);
|
|
2059
|
-
}
|
|
2060
|
-
try {
|
|
2061
|
-
await saveProgress(store, {
|
|
2062
|
-
completedFiles: p.completedFiles,
|
|
2063
|
-
depositCount,
|
|
2064
|
-
trainedContentBytes,
|
|
2065
|
-
totalBytesProcessed,
|
|
2066
|
-
totalCorpusBytes,
|
|
2067
|
-
});
|
|
2068
|
-
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2069
|
-
}
|
|
2070
|
-
catch { /* best effort — finish() will retry */ }
|
|
2071
|
-
};
|
|
2072
|
-
// ── §10d General-Knowledge stage (runs AFTER oasst2) ──
|
|
2073
|
-
//
|
|
2074
|
-
// Downloads the single JSON-array file (output.json) and deposits each
|
|
2075
|
-
// {Question, Answer} as one fact. Marked complete only when fully consumed.
|
|
2076
|
-
// LOCAL_PATH may hold a pre-downloaded *.json.
|
|
2077
|
-
const genToItems = (row) => {
|
|
2078
|
-
const r = toGenKnowRow(row);
|
|
2079
|
-
return r ? genKnowRowToItems(r) : null;
|
|
2080
|
-
};
|
|
2081
|
-
const trainGenKnow = async () => {
|
|
2082
|
-
if (!GENKNOW)
|
|
2083
|
-
return;
|
|
2084
|
-
if (trainedContentBytes >= MAX_BYTES || stopRequested)
|
|
2085
|
-
return;
|
|
2086
|
-
const p = await loadProgress(store);
|
|
2087
|
-
if (p.completedFiles.includes(GENKNOW_ID)) {
|
|
2088
|
-
progress.log(` ${DIM}· General-Knowledge already trained — skipping${R}`);
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
let path = "", downloaded = false;
|
|
2092
|
-
if (LOCAL_PATH) {
|
|
2093
|
-
const hit = readdirSync(LOCAL_PATH).find((f) => /general.*knowledge.*\.json$/i.test(f) || /output\.json$/i.test(f));
|
|
2094
|
-
if (!hit) {
|
|
2095
|
-
progress.log(` ${DIM}· no General-Knowledge *.json in ${LOCAL_PATH} — skipping${R}`);
|
|
2096
|
-
return;
|
|
2097
|
-
}
|
|
2098
|
-
path = join(LOCAL_PATH, hit);
|
|
2099
|
-
}
|
|
2100
|
-
else {
|
|
2101
|
-
const got = await acquire(GENKNOW_URL, "general_knowledge.json", "General-Knowledge");
|
|
2102
|
-
if (!got)
|
|
2103
|
-
return;
|
|
2104
|
-
path = got;
|
|
2105
|
-
downloaded = true;
|
|
2106
|
-
}
|
|
2107
|
-
try {
|
|
2108
|
-
totalCorpusBytes += statSync(path).size;
|
|
2109
|
-
}
|
|
2110
|
-
catch { /* best effort */ }
|
|
2111
|
-
state.fileTotal = 1;
|
|
2112
|
-
state.fileIndex = 1;
|
|
2113
|
-
state.activity = "process";
|
|
2114
|
-
state.filePath = "General-Knowledge";
|
|
2115
|
-
state.fileExamples = 0;
|
|
2116
|
-
tick(true);
|
|
2117
|
-
const p0 = Date.now();
|
|
2118
|
-
let res;
|
|
2119
|
-
try {
|
|
2120
|
-
res = await processJsonArray(path, genToItems, ci, onDeposit, sample);
|
|
2121
|
-
}
|
|
2122
|
-
catch (e) {
|
|
2123
|
-
if (stopRequested || e?.name === "AbortError")
|
|
2124
|
-
return;
|
|
2125
|
-
progress.log(` ${RED}✗${R} General-Knowledge processing failed: ${e.message}`);
|
|
2126
|
-
return;
|
|
2127
|
-
}
|
|
2128
|
-
langTally["genknow"] = (langTally["genknow"] ?? 0) + res.examples;
|
|
2129
|
-
progress.log(` ${GRN}✓${R} General-Knowledge ${DIM}[Q&A facts]${R} → ${int(res.examples)} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2130
|
-
(res.skipped
|
|
2131
|
-
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2132
|
-
: "") +
|
|
2133
|
-
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""));
|
|
2134
|
-
if (!res.stopped) {
|
|
2135
|
-
try {
|
|
2136
|
-
totalBytesProcessed += statSync(path).size;
|
|
2137
|
-
}
|
|
2138
|
-
catch { /* best effort */ }
|
|
2139
|
-
if (downloaded) {
|
|
2140
|
-
try {
|
|
2141
|
-
unlinkSync(path);
|
|
2142
|
-
}
|
|
2143
|
-
catch { /* best effort */ }
|
|
2144
|
-
}
|
|
2145
|
-
p.completedFiles.push(GENKNOW_ID);
|
|
2146
|
-
}
|
|
2256
|
+
progress.log(
|
|
2257
|
+
` ${CYAN}⬇${R} oasst2 trees ${bytes(sz)} ` +
|
|
2258
|
+
`${DIM}${dur(dlS)} @ ${bytes(sz / dlS)}/s${R}`,
|
|
2259
|
+
);
|
|
2260
|
+
gzPath = dest;
|
|
2261
|
+
downloaded = true;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
try {
|
|
2265
|
+
totalCorpusBytes += statSync(gzPath).size;
|
|
2266
|
+
} catch { /* best effort */ }
|
|
2267
|
+
state.fileTotal = 1;
|
|
2268
|
+
state.fileIndex = 1;
|
|
2269
|
+
// Stream the trees.
|
|
2270
|
+
state.activity = "process";
|
|
2271
|
+
state.filePath = "oasst2 (multi-turn)";
|
|
2272
|
+
state.fileExamples = 0;
|
|
2273
|
+
tick(true);
|
|
2274
|
+
const p0 = Date.now();
|
|
2275
|
+
let result;
|
|
2276
|
+
try {
|
|
2277
|
+
result = await processOasst(gzPath, ci, onDeposit, sample);
|
|
2278
|
+
} catch (e) {
|
|
2279
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
progress.log(` ${RED}✗${R} oasst2 processing failed: ${e.message}`);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
const { examples, stopped, skipped, multi } = result;
|
|
2286
|
+
langTally["oasst2"] = (langTally["oasst2"] ?? 0) + examples;
|
|
2287
|
+
progress.log(
|
|
2288
|
+
` ${GRN}✓${R} oasst2 ${DIM}[multi-turn chat]${R} → ${
|
|
2289
|
+
int(examples)
|
|
2290
|
+
} examples from ${int(multi)} conversation(s) ${DIM}in ${
|
|
2291
|
+
dur((Date.now() - p0) / 1000)
|
|
2292
|
+
}${R}` +
|
|
2293
|
+
(skipped
|
|
2294
|
+
? ` ${YEL}· ${int(skipped)} malformed line(s) skipped${R}`
|
|
2295
|
+
: "") +
|
|
2296
|
+
(stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
2297
|
+
);
|
|
2298
|
+
// Only mark complete (and reclaim the cache) when fully consumed.
|
|
2299
|
+
if (!stopped) {
|
|
2300
|
+
try {
|
|
2301
|
+
totalBytesProcessed += statSync(gzPath).size;
|
|
2302
|
+
} catch { /* best effort */ }
|
|
2303
|
+
if (downloaded) {
|
|
2147
2304
|
try {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2305
|
+
unlinkSync(gzPath);
|
|
2306
|
+
} catch { /* best effort */ }
|
|
2307
|
+
}
|
|
2308
|
+
p.completedFiles.push(OASST_ID);
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
await saveProgress(store, {
|
|
2312
|
+
completedFiles: p.completedFiles,
|
|
2313
|
+
depositCount,
|
|
2314
|
+
trainedContentBytes,
|
|
2315
|
+
totalBytesProcessed,
|
|
2316
|
+
totalCorpusBytes,
|
|
2317
|
+
});
|
|
2318
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2319
|
+
} catch { /* best effort — finish() will retry */ }
|
|
2320
|
+
};
|
|
2321
|
+
// ── §10d General-Knowledge stage (runs AFTER oasst2) ──
|
|
2322
|
+
//
|
|
2323
|
+
// Downloads the single JSON-array file (output.json) and deposits each
|
|
2324
|
+
// {Question, Answer} as one fact. Marked complete only when fully consumed.
|
|
2325
|
+
// LOCAL_PATH may hold a pre-downloaded *.json.
|
|
2326
|
+
const genToItems = (row) => {
|
|
2327
|
+
const r = toGenKnowRow(row);
|
|
2328
|
+
return r ? genKnowRowToItems(r) : null;
|
|
2329
|
+
};
|
|
2330
|
+
const trainGenKnow = async () => {
|
|
2331
|
+
if (!GENKNOW) {
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
if (trainedContentBytes >= MAX_BYTES || stopRequested) {
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
const p = await loadProgress(store);
|
|
2338
|
+
if (p.completedFiles.includes(GENKNOW_ID)) {
|
|
2339
|
+
progress.log(
|
|
2340
|
+
` ${DIM}· General-Knowledge already trained — skipping${R}`,
|
|
2341
|
+
);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
let path = "", downloaded = false;
|
|
2345
|
+
if (LOCAL_PATH) {
|
|
2346
|
+
const hit = readdirSync(LOCAL_PATH).find((f) =>
|
|
2347
|
+
/general.*knowledge.*\.json$/i.test(f) || /output\.json$/i.test(f)
|
|
2348
|
+
);
|
|
2349
|
+
if (!hit) {
|
|
2350
|
+
progress.log(
|
|
2351
|
+
` ${DIM}· no General-Knowledge *.json in ${LOCAL_PATH} — skipping${R}`,
|
|
2352
|
+
);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
path = join(LOCAL_PATH, hit);
|
|
2356
|
+
} else {
|
|
2357
|
+
const got = await acquire(
|
|
2358
|
+
GENKNOW_URL,
|
|
2359
|
+
"general_knowledge.json",
|
|
2360
|
+
"General-Knowledge",
|
|
2361
|
+
);
|
|
2362
|
+
if (!got) {
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
path = got;
|
|
2366
|
+
downloaded = true;
|
|
2367
|
+
}
|
|
2368
|
+
try {
|
|
2369
|
+
totalCorpusBytes += statSync(path).size;
|
|
2370
|
+
} catch { /* best effort */ }
|
|
2371
|
+
state.fileTotal = 1;
|
|
2372
|
+
state.fileIndex = 1;
|
|
2373
|
+
state.activity = "process";
|
|
2374
|
+
state.filePath = "General-Knowledge";
|
|
2375
|
+
state.fileExamples = 0;
|
|
2166
2376
|
tick(true);
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
depositCount = prog.depositCount;
|
|
2170
|
-
trainedContentBytes = prog.trainedContentBytes;
|
|
2171
|
-
totalBytesProcessed = prog.totalBytesProcessed;
|
|
2172
|
-
totalCorpusBytes = prog.totalCorpusBytes;
|
|
2173
|
-
rateTrained = trainedContentBytes;
|
|
2174
|
-
rateBytes = totalBytesProcessed;
|
|
2377
|
+
const p0 = Date.now();
|
|
2378
|
+
let res;
|
|
2175
2379
|
try {
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2380
|
+
res = await processJsonArray(path, genToItems, ci, onDeposit, sample);
|
|
2381
|
+
} catch (e) {
|
|
2382
|
+
if (stopRequested || e?.name === "AbortError") {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
progress.log(
|
|
2386
|
+
` ${RED}✗${R} General-Knowledge processing failed: ${e.message}`,
|
|
2387
|
+
);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
langTally["genknow"] = (langTally["genknow"] ?? 0) + res.examples;
|
|
2391
|
+
progress.log(
|
|
2392
|
+
` ${GRN}✓${R} General-Knowledge ${DIM}[Q&A facts]${R} → ${
|
|
2393
|
+
int(res.examples)
|
|
2394
|
+
} facts ${DIM}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
2395
|
+
(res.skipped
|
|
2396
|
+
? ` ${YEL}· ${int(res.skipped)} unusable row(s) skipped${R}`
|
|
2397
|
+
: "") +
|
|
2398
|
+
(res.stopped ? ` ${YEL}(stopped early)${R}` : ""),
|
|
2399
|
+
);
|
|
2400
|
+
if (!res.stopped) {
|
|
2401
|
+
try {
|
|
2402
|
+
totalBytesProcessed += statSync(path).size;
|
|
2403
|
+
} catch { /* best effort */ }
|
|
2404
|
+
if (downloaded) {
|
|
2405
|
+
try {
|
|
2406
|
+
unlinkSync(path);
|
|
2407
|
+
} catch { /* best effort */ }
|
|
2408
|
+
}
|
|
2409
|
+
p.completedFiles.push(GENKNOW_ID);
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
await saveProgress(store, {
|
|
2413
|
+
completedFiles: p.completedFiles,
|
|
2414
|
+
depositCount,
|
|
2415
|
+
trainedContentBytes,
|
|
2416
|
+
totalBytesProcessed,
|
|
2417
|
+
totalCorpusBytes,
|
|
2418
|
+
});
|
|
2419
|
+
await store.setMeta("train.langTally", JSON.stringify(langTally));
|
|
2420
|
+
} catch { /* best effort — finish() will retry */ }
|
|
2421
|
+
};
|
|
2422
|
+
// ── §10 Train the curriculum (resume-aware; one store records all stages) ──
|
|
2423
|
+
//
|
|
2424
|
+
// Every source is paged from an HTTP API (SmolSent, Aya) or a single
|
|
2425
|
+
// downloaded file (oasst2) — there is no per-file ZIP loop. Each stage closure
|
|
2426
|
+
// reads the authoritative completed-set from the store, skips itself when
|
|
2427
|
+
// already done, and persists its own progress, so the whole curriculum resumes
|
|
2428
|
+
// from the store alone. LOCAL_PATH lets oasst2 read a local *.jsonl.gz.
|
|
2429
|
+
tick(true);
|
|
2430
|
+
// ── resume — restore counters and the per-source tally from the store ──
|
|
2431
|
+
const prog = await loadProgress(store);
|
|
2432
|
+
depositCount = prog.depositCount;
|
|
2433
|
+
trainedContentBytes = prog.trainedContentBytes;
|
|
2434
|
+
totalBytesProcessed = prog.totalBytesProcessed;
|
|
2435
|
+
totalCorpusBytes = prog.totalCorpusBytes;
|
|
2436
|
+
rateTrained = trainedContentBytes;
|
|
2437
|
+
rateBytes = totalBytesProcessed;
|
|
2438
|
+
try {
|
|
2439
|
+
const t = await store.getMeta("train.langTally");
|
|
2440
|
+
if (t) {
|
|
2441
|
+
const parsed = JSON.parse(t);
|
|
2442
|
+
if (parsed && typeof parsed === "object") {
|
|
2443
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
2444
|
+
langTally[k] = Number(v) || 0;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2185
2447
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2448
|
+
} catch { /* fresh tally */ }
|
|
2449
|
+
if (prog.completedFiles.length > 0) {
|
|
2450
|
+
progress.log(
|
|
2451
|
+
` ${CYAN}↻${R} resuming: ${prog.completedFiles.length} stage-unit(s) done, ` +
|
|
2452
|
+
`${int(depositCount)} examples, ${bytes(trainedContentBytes)} learned`,
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
// Stage 1 (SmolSent translation facts), 2 (Aya multilingual chat), 3 (oasst2
|
|
2456
|
+
// multi-turn), 4 (General-Knowledge Q&A facts). Each is skipped on a resume
|
|
2457
|
+
// that already finished it.
|
|
2458
|
+
if (!stopRequested) {
|
|
2459
|
+
await trainSmolSent();
|
|
2460
|
+
}
|
|
2461
|
+
if (!stopRequested) {
|
|
2462
|
+
await trainAya();
|
|
2463
|
+
}
|
|
2464
|
+
if (!stopRequested) {
|
|
2465
|
+
await trainOasst();
|
|
2466
|
+
}
|
|
2467
|
+
if (!stopRequested) {
|
|
2468
|
+
await trainGenKnow();
|
|
2469
|
+
}
|
|
2470
|
+
await finish(stopRequested ? stopReason : "done");
|
|
2203
2471
|
}
|
|
2204
2472
|
// ═══════════════════════════════════════════════════════════════════════
|
|
2205
2473
|
// §11 Entry point — only run when invoked directly, so importing the parser
|
|
2206
2474
|
// functions above (e.g. for tests) never starts training.
|
|
2207
2475
|
// ═══════════════════════════════════════════════════════════════════════
|
|
2208
2476
|
const isMain = import.meta.url === `file://${process.argv[1]}` ||
|
|
2209
|
-
|
|
2477
|
+
process.argv[1]?.endsWith("train_base.js");
|
|
2210
2478
|
if (isMain) {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2479
|
+
main().catch((e) => {
|
|
2480
|
+
process.stderr.write(SHOW);
|
|
2481
|
+
console.error(`\n${RED}fatal:${R}`, e);
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
});
|
|
2216
2484
|
}
|