@hviana/sema 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +43 -4
- package/DATASETS.md +160 -0
- package/README.md +12 -0
- package/dist/example/train_base/cache.d.ts +35 -0
- package/dist/example/train_base/cache.js +211 -0
- package/dist/example/train_base/config.d.ts +21 -0
- package/dist/example/train_base/config.js +94 -0
- package/dist/example/train_base/corpora/aya.d.ts +19 -0
- package/dist/example/train_base/corpora/aya.js +76 -0
- package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
- package/dist/example/train_base/corpora/converted-parquet.js +44 -0
- package/dist/example/train_base/corpora/genknow.d.ts +14 -0
- package/dist/example/train_base/corpora/genknow.js +83 -0
- package/dist/example/train_base/corpora/index.d.ts +29 -0
- package/dist/example/train_base/corpora/index.js +81 -0
- package/dist/example/train_base/corpora/massive.d.ts +7 -0
- package/dist/example/train_base/corpora/massive.js +98 -0
- package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
- package/dist/example/train_base/corpora/oasst2.js +120 -0
- package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
- package/dist/example/train_base/corpora/smolsent.js +156 -0
- package/dist/example/train_base/corpora/soda.d.ts +12 -0
- package/dist/example/train_base/corpora/soda.js +113 -0
- package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
- package/dist/example/train_base/corpora/taskmaster.js +144 -0
- package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
- package/dist/example/train_base/corpora/wiki2.js +132 -0
- package/dist/example/train_base/corpus.d.ts +88 -0
- package/dist/example/train_base/corpus.js +65 -0
- package/dist/example/train_base/discovery.d.ts +48 -0
- package/dist/example/train_base/discovery.js +143 -0
- package/dist/example/train_base/http.d.ts +82 -0
- package/dist/example/train_base/http.js +219 -0
- package/dist/example/train_base/items.d.ts +46 -0
- package/dist/example/train_base/items.js +98 -0
- package/dist/example/train_base/main.d.ts +4 -0
- package/dist/example/train_base/main.js +207 -0
- package/dist/example/train_base/progress.d.ts +34 -0
- package/dist/example/train_base/progress.js +114 -0
- package/dist/example/train_base/readers.d.ts +125 -0
- package/dist/example/train_base/readers.js +391 -0
- package/dist/example/train_base/runtime.d.ts +115 -0
- package/dist/example/train_base/runtime.js +637 -0
- package/dist/example/train_base/stage.d.ts +3 -0
- package/dist/example/train_base/stage.js +246 -0
- package/dist/example/train_base/ui.d.ts +88 -0
- package/dist/example/train_base/ui.js +272 -0
- package/dist/src/geometry.d.ts +20 -0
- package/dist/src/geometry.js +22 -0
- package/dist/src/mind/attention.d.ts +6 -0
- package/dist/src/mind/attention.js +44 -4
- package/dist/src/mind/learning.js +134 -50
- package/dist/src/mind/mechanisms/cast.js +45 -1
- package/dist/src/mind/mind.d.ts +7 -2
- package/dist/src/mind/mind.js +15 -3
- package/dist/src/mind/reasoning.js +59 -5
- package/dist/src/mind/recognition.js +29 -3
- package/dist/src/mind/traverse.d.ts +16 -0
- package/dist/src/mind/traverse.js +18 -0
- package/dist/src/store-sqlite.d.ts +4 -0
- package/dist/src/store-sqlite.js +47 -0
- package/dist/src/store.d.ts +7 -0
- package/example/train_base/cache.ts +251 -0
- package/example/train_base/config.ts +128 -0
- package/example/train_base/corpora/aya.ts +106 -0
- package/example/train_base/corpora/converted-parquet.ts +64 -0
- package/example/train_base/corpora/genknow.ts +114 -0
- package/example/train_base/corpora/index.ts +88 -0
- package/example/train_base/corpora/massive.ts +111 -0
- package/example/train_base/corpora/oasst2.ts +163 -0
- package/example/train_base/corpora/smolsent.ts +203 -0
- package/example/train_base/corpora/soda.ts +130 -0
- package/example/train_base/corpora/taskmaster.ts +217 -0
- package/example/train_base/corpora/wiki2.ts +190 -0
- package/example/train_base/corpus.ts +150 -0
- package/example/train_base/discovery.ts +203 -0
- package/example/train_base/http.ts +284 -0
- package/example/train_base/items.ts +118 -0
- package/example/train_base/main.ts +240 -0
- package/example/train_base/progress.ts +149 -0
- package/example/train_base/readers.ts +505 -0
- package/example/train_base/runtime.ts +894 -0
- package/example/train_base/stage.ts +276 -0
- package/example/train_base/ui.ts +333 -0
- package/jsr.json +1 -1
- package/package.json +2 -4
- package/src/geometry.ts +23 -0
- package/src/mind/attention.ts +54 -1
- package/src/mind/learning.ts +137 -43
- package/src/mind/mechanisms/cast.ts +48 -1
- package/src/mind/mind.ts +13 -2
- package/src/mind/reasoning.ts +64 -5
- package/src/mind/recognition.ts +29 -3
- package/src/mind/traverse.ts +19 -0
- package/src/store-sqlite.ts +53 -0
- package/src/store.ts +28 -0
- package/test/13-conversation.test.mjs +1 -1
- package/test/29-counterfactual.test.mjs +43 -6
- package/test/77-company-saturation.test.mjs +302 -0
- package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
- package/test/84-composed-answer-honesty.test.mjs +137 -0
- package/test/85-answered-directly.test.mjs +126 -0
- package/test/86-cast-voices-committed.test.mjs +164 -0
- package/test/87-codominant-commitment.test.mjs +250 -0
- package/test/88-dependency-footprint.test.mjs +99 -0
- package/dist/example/train_base.d.ts +0 -93
- package/dist/example/train_base.js +0 -2269
- package/example/train_base.ts +0 -2735
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// train_base/readers.ts — bytes on disk → rows → deposits.
|
|
2
|
+
//
|
|
3
|
+
// A READER knows a container format (newline-delimited JSON, a JSON array, a
|
|
4
|
+
// Parquet file) and nothing about any corpus. It is handed a row ADAPTER and
|
|
5
|
+
// deposits whatever that adapter returns. The pairing is free: any corpus can
|
|
6
|
+
// use any reader, which is the whole point of separating them.
|
|
7
|
+
//
|
|
8
|
+
// Every reader stops at an ITEM boundary — never mid-deposit — on any of three
|
|
9
|
+
// signals: the run's abort signal, `shouldStop` (a stage budget), or `onExample`
|
|
10
|
+
// returning false (the MAX_MB cap). A stopped read leaves its file un-completed
|
|
11
|
+
// so a resume re-reads it from the top, which is safe because deposition is
|
|
12
|
+
// idempotent.
|
|
13
|
+
//
|
|
14
|
+
// THE ONLY THIRD-PARTY CODE IN THIS REPOSITORY IS BELOW, and it is LAZILY
|
|
15
|
+
// LOADED. Sema itself imports nothing outside `node:` — that is a product
|
|
16
|
+
// property, not an accident (AGENTS.md §6) — and this trainer is an EXAMPLE,
|
|
17
|
+
// not part of the library. hyparquet (+ its Snappy codec) is therefore a dev
|
|
18
|
+
// dependency, and it is loaded by a dynamic import the first time a Parquet
|
|
19
|
+
// corpus is actually read: a curriculum with no Parquet stage (SmolSent,
|
|
20
|
+
// oasst2, Taskmaster, General-Knowledge) runs with the package absent
|
|
21
|
+
// entirely, and one that needs it fails with a sentence instead of a module
|
|
22
|
+
// resolution error.
|
|
23
|
+
import { isEpisode, itemBytes } from "./items.js";
|
|
24
|
+
import { PARQUET_BATCH_BYTES } from "./config.js";
|
|
25
|
+
import { openAsBlob } from "node:fs";
|
|
26
|
+
/** Deposit a row's items: an experience via ingest(text), an episode via
|
|
27
|
+
* ingest(context, continuation). After each, the per-example callback receives
|
|
28
|
+
* the item's UTF-8 content size — the quantity the scaling suite
|
|
29
|
+
* (14-scaling.test.mjs) reports as a constant KB/s — then gates the global
|
|
30
|
+
* example count and checkpointing (returns false to stop). */
|
|
31
|
+
export async function ingestItems(ci, items, onItem, sample) {
|
|
32
|
+
for (const it of items) {
|
|
33
|
+
if (isEpisode(it))
|
|
34
|
+
await ci.ingest(it.context, it.continuation);
|
|
35
|
+
else
|
|
36
|
+
await ci.ingest(it);
|
|
37
|
+
sample?.(it);
|
|
38
|
+
if (!(await onItem(itemBytes(it))))
|
|
39
|
+
return false; // stop requested
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/** Shared tail of every reader: deposit one row's items and keep the counts. */
|
|
44
|
+
async function depositRow(row, toItems, rc, res) {
|
|
45
|
+
const items = toItems(row);
|
|
46
|
+
if (!items || items.length === 0) {
|
|
47
|
+
res.unusable++;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
res.rowsUsed++;
|
|
51
|
+
return ingestItems(rc.ci, items, async (contentBytes) => {
|
|
52
|
+
res.examples++;
|
|
53
|
+
return rc.onExample(contentBytes);
|
|
54
|
+
}, rc.sample);
|
|
55
|
+
}
|
|
56
|
+
const blank = () => ({
|
|
57
|
+
examples: 0,
|
|
58
|
+
rowsUsed: 0,
|
|
59
|
+
skipped: 0,
|
|
60
|
+
unusable: 0,
|
|
61
|
+
stopped: false,
|
|
62
|
+
});
|
|
63
|
+
/** Newline-delimited JSON, optionally gzipped.
|
|
64
|
+
*
|
|
65
|
+
* ONE reader serves both the plain JSONL sources and the gzipped oasst2 tree
|
|
66
|
+
* dump: the only difference between them is a `DecompressionStream("gzip")` in
|
|
67
|
+
* the pipeline, and duplicating an 80-line splitter to express that was how
|
|
68
|
+
* the two copies drifted apart in the first place.
|
|
69
|
+
*
|
|
70
|
+
* Lines are split without buffering the whole file OR an unbounded line: a
|
|
71
|
+
* record longer than `maxLineChars` is dropped (counted `skipped`) and the
|
|
72
|
+
* stream continues at the next newline, so a corrupt record can never exhaust
|
|
73
|
+
* memory or abort a good file. */
|
|
74
|
+
export const lines = (opts) => async (filePath, toItems, rc) => {
|
|
75
|
+
const res = blank();
|
|
76
|
+
const blob = await openAsBlob(filePath);
|
|
77
|
+
// gzip is a web standard here too (DecompressionStream), so the compressed
|
|
78
|
+
// and plain forms differ by exactly one pipe stage and nothing else.
|
|
79
|
+
const reader = (opts.gzip
|
|
80
|
+
? blob.stream()
|
|
81
|
+
.pipeThrough(new DecompressionStream("gzip"))
|
|
82
|
+
.pipeThrough(new TextDecoderStream())
|
|
83
|
+
: blob.stream()
|
|
84
|
+
.pipeThrough(new TextDecoderStream())).getReader();
|
|
85
|
+
let leftover = "", dropping = false;
|
|
86
|
+
// A "row" here is a non-blank line, counted whether or not it parses — so the
|
|
87
|
+
// position is a property of the FILE, reproducible on a later run.
|
|
88
|
+
const skip = rc.startRow ?? 0;
|
|
89
|
+
let rowIndex = 0;
|
|
90
|
+
const processLine = async (line) => {
|
|
91
|
+
if (!line.trim())
|
|
92
|
+
return true;
|
|
93
|
+
rowIndex++;
|
|
94
|
+
// Already deposited by an earlier run: advance the position, touch nothing
|
|
95
|
+
// else. Not parsed and not counted, so this read's numbers describe only
|
|
96
|
+
// the rows it actually trained.
|
|
97
|
+
if (rowIndex <= skip) {
|
|
98
|
+
rc.onRowDone?.(rowIndex);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (rc.shouldStop?.())
|
|
102
|
+
return false;
|
|
103
|
+
let row;
|
|
104
|
+
try {
|
|
105
|
+
row = JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
res.skipped++;
|
|
109
|
+
rc.onRowDone?.(rowIndex); // nothing to deposit — the row is dealt with
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const ok = await depositRow(row, toItems, rc, res);
|
|
113
|
+
if (ok)
|
|
114
|
+
rc.onRowDone?.(rowIndex); // every item landed
|
|
115
|
+
return ok;
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
while (true) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done)
|
|
121
|
+
break;
|
|
122
|
+
let chunk = value;
|
|
123
|
+
for (;;) {
|
|
124
|
+
const nl = chunk.indexOf("\n");
|
|
125
|
+
if (nl < 0) {
|
|
126
|
+
if (!dropping) {
|
|
127
|
+
if (leftover.length + chunk.length > opts.maxLineChars) {
|
|
128
|
+
leftover = "";
|
|
129
|
+
dropping = true;
|
|
130
|
+
res.skipped++;
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
leftover += chunk;
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const part = chunk.slice(0, nl);
|
|
138
|
+
chunk = chunk.slice(nl + 1);
|
|
139
|
+
if (dropping) {
|
|
140
|
+
dropping = false;
|
|
141
|
+
leftover = "";
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (leftover.length + part.length > opts.maxLineChars) {
|
|
145
|
+
leftover = "";
|
|
146
|
+
res.skipped++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const line = leftover + part;
|
|
150
|
+
leftover = "";
|
|
151
|
+
if (!(await processLine(line))) {
|
|
152
|
+
res.stopped = true;
|
|
153
|
+
return res;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!dropping && leftover.trim()) {
|
|
158
|
+
if (!(await processLine(leftover)))
|
|
159
|
+
res.stopped = true;
|
|
160
|
+
}
|
|
161
|
+
return res;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
// CANCEL, not releaseLock: a read that returns early (a budget, the MAX_MB
|
|
165
|
+
// cap, a signal) leaves the file source open otherwise, to be closed
|
|
166
|
+
// whenever the collector gets to it. Measured, that is tidiness rather than
|
|
167
|
+
// a leak — 300 abandoned reads peaked at 42 open descriptors against 38
|
|
168
|
+
// with cancel — but "closed when we are done with it" is the cheaper thing
|
|
169
|
+
// to reason about, and cancel releases the lock too.
|
|
170
|
+
try {
|
|
171
|
+
await reader.cancel();
|
|
172
|
+
}
|
|
173
|
+
catch { /* already closed */ }
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/** A whole-file JSON ARRAY of rows. The arrays this reads are small enough
|
|
177
|
+
* (~16 MB) to parse whole; a huge file would be rejected by the cache ceiling
|
|
178
|
+
* long before this. */
|
|
179
|
+
export const jsonArray = () => async (filePath, toItems, rc) => {
|
|
180
|
+
const res = blank();
|
|
181
|
+
const blob = await openAsBlob(filePath);
|
|
182
|
+
let arr;
|
|
183
|
+
try {
|
|
184
|
+
arr = JSON.parse(await blob.text());
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
throw new Error(`invalid JSON: ${e.message}`);
|
|
188
|
+
}
|
|
189
|
+
// A file that parses but is not an array is a DIFFERENT CONTAINER, not an
|
|
190
|
+
// empty one, so it is an error rather than zero rows. Silently reading it as
|
|
191
|
+
// empty marked the unit complete, logged "0 facts", and never looked again —
|
|
192
|
+
// and the live path to that is a LOCAL_PATH run, where the Taskmaster stage
|
|
193
|
+
// takes every *.json in the directory while the remote listing filters out
|
|
194
|
+
// exactly the two files (ontology.json, sample.json) that are not arrays of
|
|
195
|
+
// conversations. Throwing leaves the unit un-completed and says which file.
|
|
196
|
+
if (!Array.isArray(arr)) {
|
|
197
|
+
throw new Error(`expected a JSON array of rows, got ${arr === null ? "null" : Array.isArray(arr) ? "array" : typeof arr}`);
|
|
198
|
+
}
|
|
199
|
+
const rows = arr;
|
|
200
|
+
const skip = rc.startRow ?? 0;
|
|
201
|
+
for (let i = 0; i < rows.length; i++) {
|
|
202
|
+
if (i < skip) { // deposited by an earlier run — see ReadContext
|
|
203
|
+
rc.onRowDone?.(i + 1);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (rc.signal.aborted || rc.shouldStop?.()) {
|
|
207
|
+
res.stopped = true;
|
|
208
|
+
return res;
|
|
209
|
+
}
|
|
210
|
+
if (!(await depositRow(rows[i], toItems, rc, res))) {
|
|
211
|
+
res.stopped = true;
|
|
212
|
+
return res;
|
|
213
|
+
}
|
|
214
|
+
rc.onRowDone?.(i + 1);
|
|
215
|
+
}
|
|
216
|
+
return res;
|
|
217
|
+
};
|
|
218
|
+
/** How many rows to materialise in one read from a row-group of `rgRows` rows
|
|
219
|
+
* occupying `groupBytes` uncompressed bytes, under a `budgetBytes` target.
|
|
220
|
+
*
|
|
221
|
+
* The group's own footer statistics give the mean row width, so the batch
|
|
222
|
+
* follows the CORPUS's row size rather than the writer's layout: wide rows
|
|
223
|
+
* (SODA carries a whole dialogue per row) batch smaller than narrow ones at
|
|
224
|
+
* the same memory cost. Never exceeds the group — a batch is a subdivision of
|
|
225
|
+
* a group, never a span across two, because `parquetReadObjects` is given an
|
|
226
|
+
* absolute row range and column chunks are per-group. Never returns 0, or the
|
|
227
|
+
* read loop could not advance.
|
|
228
|
+
*
|
|
229
|
+
* A writer that omits `total_byte_size` yields `groupBytes <= 0`; the batch is
|
|
230
|
+
* then the whole group, which is exactly the behaviour this replaced. That
|
|
231
|
+
* fallback is safe for every file we read today (all three report it) and
|
|
232
|
+
* degrades to the old memory profile rather than to a wrong result. */
|
|
233
|
+
export function parquetBatchRows(rgRows, groupBytes, budgetBytes) {
|
|
234
|
+
if (!(rgRows > 0))
|
|
235
|
+
return 0; // empty group — the caller skips it
|
|
236
|
+
if (!(groupBytes > 0) || !Number.isFinite(groupBytes))
|
|
237
|
+
return rgRows;
|
|
238
|
+
const perRow = groupBytes / rgRows;
|
|
239
|
+
const fit = Math.floor(budgetBytes / perRow);
|
|
240
|
+
return Math.min(rgRows, Math.max(1, fit));
|
|
241
|
+
}
|
|
242
|
+
/** hyparquet + its Snappy codec, resolved on FIRST USE and remembered.
|
|
243
|
+
*
|
|
244
|
+
* A pure-JS, dependency-free Parquet reader driven over a web-standard Blob
|
|
245
|
+
* byte source. It is the one thing the web platform cannot do alone, and the
|
|
246
|
+
* one package this repository pulls in — so it is loaded here, where it is
|
|
247
|
+
* used, rather than at module scope, where it would become a load-time
|
|
248
|
+
* requirement of the whole trainer. */
|
|
249
|
+
let parquetLib = null;
|
|
250
|
+
const loadParquet = () => (parquetLib ??= (async () => {
|
|
251
|
+
try {
|
|
252
|
+
const [hp, hc] = await Promise.all([
|
|
253
|
+
import("hyparquet"),
|
|
254
|
+
import("hyparquet-compressors"),
|
|
255
|
+
]);
|
|
256
|
+
return {
|
|
257
|
+
metadata: hp.parquetMetadataAsync,
|
|
258
|
+
readObjects: hp.parquetReadObjects,
|
|
259
|
+
compressors: hc.compressors,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
// A missing package here is a SETUP problem with a specific remedy, so say
|
|
264
|
+
// the remedy. Anything else (a genuine load error inside the package) is
|
|
265
|
+
// rethrown untouched.
|
|
266
|
+
const code = e?.code;
|
|
267
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
268
|
+
throw new Error("this corpus ships as Parquet and needs the hyparquet reader, which " +
|
|
269
|
+
"is a DEV dependency of this example (the library itself has none). " +
|
|
270
|
+
"Run `npm install` in the repository, or disable the Parquet " +
|
|
271
|
+
"stages: AYA=0 WIKI2=0 SODA=0 MASSIVE=0.");
|
|
272
|
+
}
|
|
273
|
+
throw e;
|
|
274
|
+
}
|
|
275
|
+
})());
|
|
276
|
+
/** The top-level column names a Parquet file actually carries, taken from the
|
|
277
|
+
* chunk metadata (`path_in_schema[0]` is the top-level name, which is exactly
|
|
278
|
+
* what hyparquet matches a `columns` request against). */
|
|
279
|
+
const columnsOf = (meta) => {
|
|
280
|
+
const out = new Set();
|
|
281
|
+
for (const rg of meta.row_groups) {
|
|
282
|
+
for (const c of rg.columns) {
|
|
283
|
+
const top = c.meta_data?.path_in_schema?.[0];
|
|
284
|
+
if (top)
|
|
285
|
+
out.add(top);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
};
|
|
290
|
+
/** Reject a column projection that names a column the file does not have.
|
|
291
|
+
*
|
|
292
|
+
* hyparquet ignores an unknown name in `columns` rather than complaining, so a
|
|
293
|
+
* typo would read NO columns, hand every adapter an empty row, and finish with
|
|
294
|
+
* "0 facts" and no error at all. A projection is a claim about the file, so a
|
|
295
|
+
* wrong claim is worth stopping for — and the message names both what is
|
|
296
|
+
* missing and what is there, which is what you need to fix it. */
|
|
297
|
+
function checkColumns(available, want) {
|
|
298
|
+
const missing = want.filter((c) => !available.has(c));
|
|
299
|
+
if (missing.length > 0) {
|
|
300
|
+
throw new Error(`column(s) not in this file: ${missing.join(", ")} — ` +
|
|
301
|
+
`it carries ${[...available].join(", ")}`);
|
|
302
|
+
}
|
|
303
|
+
return want;
|
|
304
|
+
}
|
|
305
|
+
/** Parquet, read in bounded row batches with hyparquet (+Snappy from
|
|
306
|
+
* hyparquet-compressors) over a web-standard Blob byte source. At most
|
|
307
|
+
* `batchBytes` of source rows are materialised at a time, so neither a
|
|
308
|
+
* multi-hundred-MB file nor a file written as ONE giant row-group loads whole
|
|
309
|
+
* into memory.
|
|
310
|
+
*
|
|
311
|
+
* Batching also makes a single-group file INTERRUPTIBLE: the abort check runs
|
|
312
|
+
* per batch, where before a 1.19M-row group could not be cancelled at all.
|
|
313
|
+
*
|
|
314
|
+
* `columns` PROJECTS the read down to the columns the adapter actually uses.
|
|
315
|
+
* That is not only a memory economy: 2Wiki's `context` column holds the
|
|
316
|
+
* Wikipedia prose the adapter exists to avoid depositing, and naming the
|
|
317
|
+
* columns makes that exclusion structural — the bytes are never decoded at
|
|
318
|
+
* all — in the same way reading only `utterances[].text` structurally excludes
|
|
319
|
+
* Taskmaster's `instructions` scaffolding. Absent ⇒ every column, as before. */
|
|
320
|
+
export const parquet = (opts = {}) => async (filePath, toItems, rc) => {
|
|
321
|
+
const { metadata, readObjects, compressors } = await loadParquet();
|
|
322
|
+
const budget = opts.batchBytes ?? PARQUET_BATCH_BYTES;
|
|
323
|
+
const res = blank();
|
|
324
|
+
const blob = await openAsBlob(filePath);
|
|
325
|
+
const file = {
|
|
326
|
+
byteLength: blob.size,
|
|
327
|
+
slice: async (start, end) => await blob.slice(start, end ?? blob.size).arrayBuffer(),
|
|
328
|
+
};
|
|
329
|
+
const meta = await metadata(file);
|
|
330
|
+
const columns = opts.columns
|
|
331
|
+
? checkColumns(columnsOf(meta), opts.columns)
|
|
332
|
+
: undefined;
|
|
333
|
+
// A "row" here is the file's ABSOLUTE row index, which is what hyparquet's
|
|
334
|
+
// rowStart/rowEnd already speak in — so resuming is not merely cheaper than
|
|
335
|
+
// re-reading, it decodes nothing at all before the cursor.
|
|
336
|
+
const skip = rc.startRow ?? 0;
|
|
337
|
+
let rowStart = 0;
|
|
338
|
+
for (const rg of meta.row_groups) {
|
|
339
|
+
const rgRows = Number(rg.num_rows);
|
|
340
|
+
const rgEnd = rowStart + rgRows;
|
|
341
|
+
if (rgEnd <= skip) {
|
|
342
|
+
// Entirely behind the cursor: never fetched, never decompressed.
|
|
343
|
+
rowStart = rgEnd;
|
|
344
|
+
rc.onRowDone?.(rowStart);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// `total_byte_size` covers EVERY column, including ones a projection skips,
|
|
348
|
+
// so a projected read materialises less than the budget rather than more.
|
|
349
|
+
// Erring small is the safe direction for a memory budget, and correcting it
|
|
350
|
+
// per-column would tie the batch size to a layout detail for no gain.
|
|
351
|
+
const batchRows = parquetBatchRows(rgRows, Number(rg.total_byte_size ?? 0), budget);
|
|
352
|
+
if (batchRows <= 0)
|
|
353
|
+
continue; // empty group
|
|
354
|
+
// Resume inside a group: begin at the cursor, not at the group's first row.
|
|
355
|
+
if (rowStart < skip)
|
|
356
|
+
rowStart = skip;
|
|
357
|
+
// Materialise one bounded batch at a time, then deposit its rows.
|
|
358
|
+
while (rowStart < rgEnd) {
|
|
359
|
+
if (rc.signal.aborted || rc.shouldStop?.()) {
|
|
360
|
+
res.stopped = true;
|
|
361
|
+
return res;
|
|
362
|
+
}
|
|
363
|
+
const batchStart = rowStart;
|
|
364
|
+
const rowEnd = Math.min(rowStart + batchRows, rgEnd);
|
|
365
|
+
const rows = await readObjects({
|
|
366
|
+
file,
|
|
367
|
+
// Hand back the footer we already parsed: without it every batch
|
|
368
|
+
// re-reads and re-parses the file's metadata, which on a large shard
|
|
369
|
+
// means dozens of redundant footer parses per file.
|
|
370
|
+
metadata: meta,
|
|
371
|
+
compressors,
|
|
372
|
+
columns,
|
|
373
|
+
rowStart,
|
|
374
|
+
rowEnd,
|
|
375
|
+
});
|
|
376
|
+
rowStart = rowEnd;
|
|
377
|
+
for (let i = 0; i < rows.length; i++) {
|
|
378
|
+
if (rc.shouldStop?.()) {
|
|
379
|
+
res.stopped = true;
|
|
380
|
+
return res;
|
|
381
|
+
}
|
|
382
|
+
if (!(await depositRow(rows[i], toItems, rc, res))) {
|
|
383
|
+
res.stopped = true;
|
|
384
|
+
return res;
|
|
385
|
+
}
|
|
386
|
+
rc.onRowDone?.(batchStart + i + 1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return res;
|
|
391
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type CachedIngest, type Mind, type Store } from "../../src/index.js";
|
|
2
|
+
import type { TrainingItem } from "./items.js";
|
|
3
|
+
import { type HttpOptions } from "./http.js";
|
|
4
|
+
import type { ReadContext } from "./readers.js";
|
|
5
|
+
import { type SavedProgress } from "./progress.js";
|
|
6
|
+
import { Progress, type ProgState } from "./ui.js";
|
|
7
|
+
/** A single process-wide abort signal. SIGINT/SIGTERM aborts it, which cancels
|
|
8
|
+
* every in-flight fetch immediately (instead of waiting out a slow socket), so
|
|
9
|
+
* Ctrl+C is responsive even mid-download. The deposit loop also polls it to
|
|
10
|
+
* stop cleanly at the next item boundary, leaving the store consistent. */
|
|
11
|
+
export declare const shutdown: AbortController;
|
|
12
|
+
/** The totals a run accumulates. Mutable BY DESIGN — see the file header. */
|
|
13
|
+
export interface Counters {
|
|
14
|
+
depositCount: number;
|
|
15
|
+
trainedContentBytes: number;
|
|
16
|
+
totalBytesProcessed: number;
|
|
17
|
+
totalCorpusBytes: number;
|
|
18
|
+
langTally: Record<string, number>;
|
|
19
|
+
/** Rows a BUDGETED corpus has already taken, by corpus id, counting only
|
|
20
|
+
* units it finished. A budget is a property of the STORE, not of a run, so
|
|
21
|
+
* it has to survive a resume — see the budget notes in stage.ts. Absent from
|
|
22
|
+
* an older store, which reads as 0 and reproduces the previous behaviour. */
|
|
23
|
+
rowsTaken: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
export interface TrainCtx {
|
|
26
|
+
readonly store: Store;
|
|
27
|
+
readonly mind: Mind;
|
|
28
|
+
readonly ci: CachedIngest;
|
|
29
|
+
readonly progress: Progress;
|
|
30
|
+
readonly state: ProgState;
|
|
31
|
+
readonly counters: Counters;
|
|
32
|
+
/** Network options for a LISTING call: cancellable, and its throttle waits
|
|
33
|
+
* are surfaced into the run log. Downloads and HEAD probes deliberately do
|
|
34
|
+
* NOT carry the notifier — they wait silently, as they always have. */
|
|
35
|
+
readonly http: HttpOptions;
|
|
36
|
+
/** Set once a stop has been requested (a signal, or the MAX_MB cap). Every
|
|
37
|
+
* stage checks it at its file and item boundaries. */
|
|
38
|
+
stopRequested: boolean;
|
|
39
|
+
stopReason: string;
|
|
40
|
+
/** Repaint the panel. `force` bypasses the frame-rate limiter. */
|
|
41
|
+
tick(force?: boolean): void;
|
|
42
|
+
/** Offer an item to the reservoir behind the checkpoint recall box. */
|
|
43
|
+
sample(it: TrainingItem): void;
|
|
44
|
+
/** The per-deposit gate: counts, checkpoints, and returns false to stop. */
|
|
45
|
+
onDeposit(contentBytes: number): Promise<boolean>;
|
|
46
|
+
/** Bind a reader to this run, for the unit recorded as `unitId`, resuming at
|
|
47
|
+
* `startRow`. `shouldStop` is a stage BUDGET; the MAX_MB cap and the
|
|
48
|
+
* shutdown signal reach the reader by other routes.
|
|
49
|
+
*
|
|
50
|
+
* Naming the unit is what makes the cursor meaningful: a cursor is only ever
|
|
51
|
+
* applied to the unit it was taken from. */
|
|
52
|
+
readCtx(opts: {
|
|
53
|
+
unitId: string;
|
|
54
|
+
/** Which corpus these deposits are tallied to. */
|
|
55
|
+
corpusId: string;
|
|
56
|
+
startRow?: number;
|
|
57
|
+
shouldStop?: () => boolean;
|
|
58
|
+
/** Live budget count for this corpus, for the cursor snapshot only. */
|
|
59
|
+
rowsTakenNow?: () => number;
|
|
60
|
+
}): ReadContext;
|
|
61
|
+
/** The durable position a previous run reached inside a unit it did not
|
|
62
|
+
* finish, or null. A stage applies it only to the matching unit. */
|
|
63
|
+
readonly resumeCursor: UnitCursor | null;
|
|
64
|
+
/** Reuse a cached copy, else download into the cache. Null on failure.
|
|
65
|
+
* `cached` says which happened — the stages disagree about whether a file
|
|
66
|
+
* they did not download is theirs to delete afterwards, so the answer has
|
|
67
|
+
* to reach the caller rather than being decided here. */
|
|
68
|
+
acquire(url: string, destName: string, label: string): Promise<AcquiredFile | null>;
|
|
69
|
+
/** Persist the resume record: completed units, counters, per-corpus tally,
|
|
70
|
+
* and the in-flight unit's cursor. `unitDone` CLEARS the cursor — the unit
|
|
71
|
+
* is recorded in `completedFiles` now, so a position inside it is meaningless
|
|
72
|
+
* and must not be left behind for the next run to apply. */
|
|
73
|
+
persist(completedFiles: string[], unitDone?: boolean): Promise<void>;
|
|
74
|
+
/** Restore counters and the tally from the store, and announce the resume. */
|
|
75
|
+
restore(): Promise<SavedProgress>;
|
|
76
|
+
/** Final checkpoint, summary line, and exit. */
|
|
77
|
+
finish(why: string): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
/** A durable position inside a unit that is not finished, together with the
|
|
80
|
+
* counters that describe exactly the data behind it.
|
|
81
|
+
*
|
|
82
|
+
* The counters travel WITH the position for one reason: they have to agree. A
|
|
83
|
+
* resume that restored the store's running totals but re-read the unit from the
|
|
84
|
+
* top counted the same deposits twice, and every interruption inflated the
|
|
85
|
+
* figures again — measured at +77% (16,000 examples reported as 28,283) after
|
|
86
|
+
* six interruptions of one corpus. Restoring the pair together makes the
|
|
87
|
+
* reported numbers describe the store, however many times it was interrupted.
|
|
88
|
+
*
|
|
89
|
+
* `rows` is what the reader counts, so its unit is reader-specific — non-blank
|
|
90
|
+
* lines for `lines`, array index for `jsonArray`, absolute file row for
|
|
91
|
+
* `parquet`. That is safe because a corpus never changes reader, and it is why
|
|
92
|
+
* a cursor names the unit it came from. */
|
|
93
|
+
export interface UnitCursor {
|
|
94
|
+
unitId: string;
|
|
95
|
+
rows: number;
|
|
96
|
+
depositCount: number;
|
|
97
|
+
trainedContentBytes: number;
|
|
98
|
+
totalBytesProcessed: number;
|
|
99
|
+
langTally: Record<string, number>;
|
|
100
|
+
rowsTaken: Record<string, number>;
|
|
101
|
+
}
|
|
102
|
+
/** The outcome of {@link TrainCtx.acquire}. */
|
|
103
|
+
export interface AcquiredFile {
|
|
104
|
+
path: string;
|
|
105
|
+
/** True when the file was already in the cache and nothing was fetched. */
|
|
106
|
+
cached: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface RuntimeOptions {
|
|
109
|
+
store: Store;
|
|
110
|
+
mind: Mind;
|
|
111
|
+
ci: CachedIngest;
|
|
112
|
+
/** Names the curriculum in the panel header. */
|
|
113
|
+
title: string;
|
|
114
|
+
}
|
|
115
|
+
export declare function createRuntime(opts: RuntimeOptions): TrainCtx;
|